cb-cookie-manager 0.0.1-security → 1.0.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.
Potentially problematic release.
This version of cb-cookie-manager might be problematic. Click here for more details.
- package/.eslintignore +11 -0
- package/LICENSE.md +13 -0
- package/README.md +477 -3
- package/dist/CookieContext.d.ts +10 -0
- package/dist/CookieContext.js +182 -0
- package/dist/TrackingManagerContext.d.ts +6 -0
- package/dist/TrackingManagerContext.js +53 -0
- package/dist/analytics.js +119 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.js +32 -0
- package/dist/examples/config.d.ts +28 -0
- package/dist/examples/config.js +77 -0
- package/dist/hooks/useHasConsent.d.ts +2 -0
- package/dist/hooks/useHasConsent.js +14 -0
- package/dist/hooks/useRequiredCategories.d.ts +3 -0
- package/dist/hooks/useRequiredCategories.js +10 -0
- package/dist/hooks/useSavedTrackingPreference.d.ts +3 -0
- package/dist/hooks/useSavedTrackingPreference.js +38 -0
- package/dist/hooks/useSetTrackingPreference.d.ts +3 -0
- package/dist/hooks/useSetTrackingPreference.js +27 -0
- package/dist/hooks/useTrackingPreference.d.ts +3 -0
- package/dist/hooks/useTrackingPreference.js +16 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +39 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +28 -0
- package/dist/utils/areCookiesEnabled.d.ts +2 -0
- package/dist/utils/areCookiesEnabled.js +14 -0
- package/dist/utils/getAllCookies.d.ts +3 -0
- package/dist/utils/getAllCookies.js +64 -0
- package/dist/utils/getDefaultTrackingPreference.d.ts +3 -0
- package/dist/utils/getDefaultTrackingPreference.js +28 -0
- package/dist/utils/getDomain.d.ts +2 -0
- package/dist/utils/getDomain.js +21 -0
- package/dist/utils/getTrackerCategory.d.ts +3 -0
- package/dist/utils/getTrackerCategory.js +11 -0
- package/dist/utils/getTrackerInfo.d.ts +3 -0
- package/dist/utils/getTrackerInfo.js +12 -0
- package/dist/utils/hasConsent.d.ts +11 -0
- package/dist/utils/hasConsent.js +29 -0
- package/dist/utils/isMaxKBSize.d.ts +2 -0
- package/dist/utils/isMaxKBSize.js +7 -0
- package/dist/utils/isOptOut.d.ts +16 -0
- package/dist/utils/isOptOut.js +25 -0
- package/dist/utils/persistMobileAppPreferences.d.ts +2 -0
- package/dist/utils/persistMobileAppPreferences.js +36 -0
- package/dist/utils/setGTMVariables.d.ts +3 -0
- package/dist/utils/setGTMVariables.js +14 -0
- package/dist/utils/setTrackingPreference.d.ts +4 -0
- package/dist/utils/setTrackingPreference.js +22 -0
- package/dist/utils/trackerMatches.d.ts +3 -0
- package/dist/utils/trackerMatches.js +12 -0
- package/jest.config.ts +204 -0
- package/package.json +30 -3
- package/src/CookieContext.test.tsx +105 -0
- package/src/CookieContext.tsx +215 -0
- package/src/TrackingManagerContext.tsx +25 -0
- package/src/analytics.ts +65 -0
- package/src/constants.ts +35 -0
- package/src/examples/config.ts +76 -0
- package/src/global.d.ts +3 -0
- package/src/hooks/useHasConsent.ts +11 -0
- package/src/hooks/useRequiredCaregories.test.tsx +43 -0
- package/src/hooks/useRequiredCategories.ts +11 -0
- package/src/hooks/useSavedTrackingPreference.ts +47 -0
- package/src/hooks/useSetTrackingPreference.test.tsx +82 -0
- package/src/hooks/useSetTrackingPreference.ts +31 -0
- package/src/hooks/useTrackingPreference.ts +15 -0
- package/src/index.ts +20 -0
- package/src/types.ts +71 -0
- package/src/utils/areCookiesEnabled.ts +13 -0
- package/src/utils/getAllCookies.test.ts +35 -0
- package/src/utils/getAllCookies.ts +68 -0
- package/src/utils/getDefaultTrackingPreference.test.ts +16 -0
- package/src/utils/getDefaultTrackingPreference.ts +28 -0
- package/src/utils/getDomain.test.ts +16 -0
- package/src/utils/getDomain.ts +19 -0
- package/src/utils/getTrackerCategory.test.ts +34 -0
- package/src/utils/getTrackerCategory.ts +11 -0
- package/src/utils/getTrackerInfo.test.ts +11 -0
- package/src/utils/getTrackerInfo.ts +10 -0
- package/src/utils/hasConsent.test.ts +64 -0
- package/src/utils/hasConsent.ts +32 -0
- package/src/utils/isMaxKBSize.test.ts +10 -0
- package/src/utils/isMaxKBSize.ts +7 -0
- package/src/utils/isOptOut.test.ts +14 -0
- package/src/utils/isOptOut.ts +28 -0
- package/src/utils/persistMobileAppPreferences.ts +32 -0
- package/src/utils/setGTMVariables.test.ts +20 -0
- package/src/utils/setGTMVariables.ts +17 -0
- package/src/utils/setTrackingPreference.ts +36 -0
- package/src/utils/trackerMatches.test.ts +13 -0
- package/src/utils/trackerMatches.ts +12 -0
- package/tsconfig.json +112 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { log } from 'console';
|
|
3
|
+
import Cookies from 'js-cookie';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import config from '../examples/config';
|
|
7
|
+
import { Provider } from '../TrackingManagerContext';
|
|
8
|
+
import { Region, TrackingCategory } from '../types';
|
|
9
|
+
import useSetTrackingPreference from './useSetTrackingPreference';
|
|
10
|
+
|
|
11
|
+
jest.mock('js-cookie', () => ({
|
|
12
|
+
__esModule: true,
|
|
13
|
+
default: {
|
|
14
|
+
get: jest.fn(),
|
|
15
|
+
set: jest.fn(),
|
|
16
|
+
remove: jest.fn(),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('useSetTrackingPreference', () => {
|
|
21
|
+
const onError = jest.fn();
|
|
22
|
+
const onPreferenceChange = jest.fn();
|
|
23
|
+
const region = Region.DEFAULT;
|
|
24
|
+
const renderFormHook = () =>
|
|
25
|
+
renderHook(() => useSetTrackingPreference(), {
|
|
26
|
+
wrapper: ({ children }: { children: React.ReactNode }) => (
|
|
27
|
+
<Provider
|
|
28
|
+
onError={onError}
|
|
29
|
+
config={config}
|
|
30
|
+
locale="en"
|
|
31
|
+
projectName="consumer-www"
|
|
32
|
+
region={region}
|
|
33
|
+
log={log}
|
|
34
|
+
onPreferenceChange={onPreferenceChange}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</Provider>
|
|
38
|
+
),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('adds required category if not set', () => {
|
|
42
|
+
const { result } = renderFormHook();
|
|
43
|
+
act(() => {
|
|
44
|
+
result.current({
|
|
45
|
+
region: Region.DEFAULT,
|
|
46
|
+
consent: [TrackingCategory.FUNCTIONAL],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(onError).toHaveBeenCalled();
|
|
51
|
+
expect(onPreferenceChange).toHaveBeenCalledWith({
|
|
52
|
+
region: 'DEFAULT',
|
|
53
|
+
consent: ['functional', 'necessary'],
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('sets new consent', () => {
|
|
58
|
+
const mockGet = Cookies.get as jest.MockedFunction<typeof Cookies.get>;
|
|
59
|
+
|
|
60
|
+
mockGet.mockImplementation(() => ({
|
|
61
|
+
cm_default_preferences: JSON.stringify({
|
|
62
|
+
region: Region.DEFAULT,
|
|
63
|
+
consent: [TrackingCategory.NECESSARY, TrackingCategory.PERFORMANCE],
|
|
64
|
+
}),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const { result } = renderFormHook();
|
|
68
|
+
const newPreference = {
|
|
69
|
+
region: Region.DEFAULT,
|
|
70
|
+
consent: [
|
|
71
|
+
TrackingCategory.NECESSARY,
|
|
72
|
+
TrackingCategory.PERFORMANCE,
|
|
73
|
+
TrackingCategory.FUNCTIONAL,
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
act(() => {
|
|
77
|
+
result.current(newPreference);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(onPreferenceChange).toHaveBeenCalledWith(newPreference);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useSetCookie } from '../CookieContext';
|
|
4
|
+
import { useTrackingManager } from '../TrackingManagerContext';
|
|
5
|
+
import { TrackingPreference } from '../types';
|
|
6
|
+
import setTrackingPreference from '../utils/setTrackingPreference';
|
|
7
|
+
import useRequiredCategories from './useRequiredCategories';
|
|
8
|
+
|
|
9
|
+
const useSetTrackingPreference = (): ((preference: TrackingPreference) => void) => {
|
|
10
|
+
const { region, onPreferenceChange, onError } = useTrackingManager();
|
|
11
|
+
const setCookie = useSetCookie();
|
|
12
|
+
const requiredCategories = useRequiredCategories();
|
|
13
|
+
|
|
14
|
+
const changePreference = useCallback(
|
|
15
|
+
(preference: TrackingPreference) => {
|
|
16
|
+
const preferencesToSet = preference;
|
|
17
|
+
requiredCategories.forEach((c) => {
|
|
18
|
+
if (!preference.consent.includes(c)) {
|
|
19
|
+
onError(new Error(`Trying to remove category ${c} that is required`));
|
|
20
|
+
preferencesToSet.consent = [...preference.consent, c];
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
setTrackingPreference(setCookie, preferencesToSet, region, onPreferenceChange);
|
|
24
|
+
},
|
|
25
|
+
[onError, onPreferenceChange, region, requiredCategories, setCookie]
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return changePreference;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default useSetTrackingPreference;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useTrackingManager } from '../TrackingManagerContext';
|
|
2
|
+
import { TrackingPreference } from '../types';
|
|
3
|
+
import getDefaultTrackingPreference from '../utils/getDefaultTrackingPreference';
|
|
4
|
+
import { useSavedTrackingPreference } from './useSavedTrackingPreference';
|
|
5
|
+
|
|
6
|
+
const useTrackingPreference = (): TrackingPreference => {
|
|
7
|
+
const preference = useSavedTrackingPreference();
|
|
8
|
+
const { region, config } = useTrackingManager();
|
|
9
|
+
|
|
10
|
+
if (!preference) return getDefaultTrackingPreference(region, config);
|
|
11
|
+
|
|
12
|
+
return preference;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default useTrackingPreference;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { TRACKER_CATEGORIES } from './constants';
|
|
2
|
+
export { useCookie } from './CookieContext';
|
|
3
|
+
export { default as useHasConsent } from './hooks/useHasConsent';
|
|
4
|
+
export { default as useRequiredCategories } from './hooks/useRequiredCategories';
|
|
5
|
+
export {
|
|
6
|
+
useSavedTrackingPreference,
|
|
7
|
+
useSavedTrackingPreferenceFromMobileApp,
|
|
8
|
+
} from './hooks/useSavedTrackingPreference';
|
|
9
|
+
export { default as useSetTrackingPreference } from './hooks/useSetTrackingPreference';
|
|
10
|
+
export { default as useTrackingPreference } from './hooks/useTrackingPreference';
|
|
11
|
+
export { Provider } from './TrackingManagerContext';
|
|
12
|
+
export { useTrackingManager } from './TrackingManagerContext';
|
|
13
|
+
export { Framework, Region, TrackerType, TrackingCategory, TrackingPreference } from './types';
|
|
14
|
+
export { default as areCookiesEnabled } from './utils/areCookiesEnabled';
|
|
15
|
+
export { default as getDefaultTrackingPreference } from './utils/getDefaultTrackingPreference';
|
|
16
|
+
export { default as isOptOut } from './utils/isOptOut';
|
|
17
|
+
export {
|
|
18
|
+
getIsMobileAppFromQueryParams,
|
|
19
|
+
persistMobileAppPreferences,
|
|
20
|
+
} from './utils/persistMobileAppPreferences';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { CookieAttributes } from 'js-cookie';
|
|
2
|
+
|
|
3
|
+
export type ErrorFunction = (error: Error, metadata?: { [key: string]: any }) => void;
|
|
4
|
+
|
|
5
|
+
export type SetCookieFunction = (value: any, options?: CookieAttributes) => void;
|
|
6
|
+
|
|
7
|
+
export type LogFunction = (str: string, options?: Record<string, any>) => void;
|
|
8
|
+
|
|
9
|
+
export enum Region {
|
|
10
|
+
EU = 'EU',
|
|
11
|
+
DEFAULT = 'DEFAULT',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export enum TrackingCategory {
|
|
15
|
+
FUNCTIONAL = 'functional',
|
|
16
|
+
NECESSARY = 'necessary',
|
|
17
|
+
TARGETING = 'targeting',
|
|
18
|
+
PERFORMANCE = 'performance',
|
|
19
|
+
DELETE_IF_SEEN = 'delete-if-seen',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type TrackingPreference = {
|
|
23
|
+
region: Region;
|
|
24
|
+
consent: Array<TrackingCategory>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export enum Framework {
|
|
28
|
+
OPT_OUT = 'optOut',
|
|
29
|
+
OPT_IN = 'optIn',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type GeolocationRule = { region: Region; framework: Framework };
|
|
33
|
+
|
|
34
|
+
export enum TrackerType {
|
|
35
|
+
COOKIE = 'cookie',
|
|
36
|
+
QUERY = 'query',
|
|
37
|
+
PIXEL = 'pixel',
|
|
38
|
+
BEACON = 'beacon',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type Tracker = {
|
|
42
|
+
id: string;
|
|
43
|
+
type: TrackerType;
|
|
44
|
+
regex?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ConfigCategoryInfo = {
|
|
48
|
+
id: TrackingCategory;
|
|
49
|
+
trackers: Array<Tracker>;
|
|
50
|
+
required?: boolean;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type Config = {
|
|
54
|
+
categories: Array<ConfigCategoryInfo>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type TrackingManagerDependencies = {
|
|
58
|
+
onError: ErrorFunction;
|
|
59
|
+
projectName: string;
|
|
60
|
+
locale: string;
|
|
61
|
+
region: Region;
|
|
62
|
+
onPreferenceChange?: (preference: TrackingPreference) => void;
|
|
63
|
+
config: Config;
|
|
64
|
+
shadowMode?: boolean;
|
|
65
|
+
log: LogFunction;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type AdTrackingPreference = {
|
|
69
|
+
value: boolean;
|
|
70
|
+
updated_at?: number;
|
|
71
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Cookies from 'js-cookie';
|
|
2
|
+
|
|
3
|
+
const TEST_COOKIE = 'test_cookie';
|
|
4
|
+
|
|
5
|
+
const areCookiesEnabled = (): boolean => {
|
|
6
|
+
Cookies.set(TEST_COOKIE, 'test');
|
|
7
|
+
const result = !!Cookies.get(TEST_COOKIE);
|
|
8
|
+
Cookies.remove(TEST_COOKIE);
|
|
9
|
+
|
|
10
|
+
return result;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default areCookiesEnabled;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Cookies from 'js-cookie';
|
|
2
|
+
|
|
3
|
+
import getAllCookies from './getAllCookies';
|
|
4
|
+
|
|
5
|
+
jest.mock('js-cookie', () => ({
|
|
6
|
+
__esModule: true,
|
|
7
|
+
default: {
|
|
8
|
+
get: jest.fn(),
|
|
9
|
+
set: jest.fn(),
|
|
10
|
+
remove: jest.fn(),
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe('getAllCookies', () => {
|
|
15
|
+
it('deseralizes cookies of all types', () => {
|
|
16
|
+
const mockGet = Cookies.get as jest.MockedFunction<typeof Cookies.get>;
|
|
17
|
+
const value = {
|
|
18
|
+
region: 'DEFAULT',
|
|
19
|
+
consent: ['necessary', 'performance'],
|
|
20
|
+
};
|
|
21
|
+
const cookies = {
|
|
22
|
+
cm_default_preferences: JSON.stringify(value),
|
|
23
|
+
some_cookie: 'iamastring',
|
|
24
|
+
another_cookie: '5',
|
|
25
|
+
array_cookie: JSON.stringify(['item1', 'item2']),
|
|
26
|
+
};
|
|
27
|
+
mockGet.mockImplementation(jest.fn(() => cookies));
|
|
28
|
+
expect(getAllCookies({})).toEqual({
|
|
29
|
+
cm_default_preferences: value,
|
|
30
|
+
some_cookie: 'iamastring',
|
|
31
|
+
another_cookie: 5,
|
|
32
|
+
array_cookie: ['item1', 'item2'],
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import Cookies from 'js-cookie';
|
|
2
|
+
|
|
3
|
+
export const deserializeCookies = (cookies: Record<string, string>) => {
|
|
4
|
+
const parsedCookies: Record<string, any> = {};
|
|
5
|
+
|
|
6
|
+
Object.keys(cookies).forEach((c) => {
|
|
7
|
+
try {
|
|
8
|
+
parsedCookies[c] = JSON.parse(cookies[c]);
|
|
9
|
+
} catch (e) {
|
|
10
|
+
parsedCookies[c] = cookies[c];
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return parsedCookies;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function getAllCookies(initialCookies?: Record<string, string>) {
|
|
17
|
+
if (typeof window === 'undefined' && initialCookies) {
|
|
18
|
+
return deserializeCookies(initialCookies);
|
|
19
|
+
}
|
|
20
|
+
return deserializeCookies(Cookies.get() || {});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function areRecordsEqual(
|
|
24
|
+
record1: Record<string, any>,
|
|
25
|
+
record2: Record<string, any>
|
|
26
|
+
): boolean {
|
|
27
|
+
// Check if the number of keys is the same
|
|
28
|
+
const keys1 = Object.keys(record1);
|
|
29
|
+
const keys2 = Object.keys(record2);
|
|
30
|
+
|
|
31
|
+
if (keys1.length !== keys2.length) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if all keys and their values are equal
|
|
36
|
+
for (const key of keys1) {
|
|
37
|
+
if (!deepEqual(record1[key], record2[key])) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function deepEqual(value1: any, value2: any): boolean {
|
|
46
|
+
if (typeof value1 !== typeof value2) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof value1 === 'object' && value1 !== null) {
|
|
51
|
+
const keys1 = Object.keys(value1);
|
|
52
|
+
const keys2 = Object.keys(value2);
|
|
53
|
+
|
|
54
|
+
if (keys1.length !== keys2.length) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const key of keys1) {
|
|
59
|
+
if (!deepEqual(value1[key], value2[key])) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} else if (value1 !== value2) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import config from '../examples/config';
|
|
2
|
+
import { Region } from '../types';
|
|
3
|
+
import getDefaultTrackingPreference from './getDefaultTrackingPreference';
|
|
4
|
+
|
|
5
|
+
describe('getDefaultTrackingPreference', () => {
|
|
6
|
+
it('returns the correct default preferences', () => {
|
|
7
|
+
expect(getDefaultTrackingPreference(Region.DEFAULT, config)).toEqual({
|
|
8
|
+
region: Region.DEFAULT,
|
|
9
|
+
consent: ['necessary', 'performance', 'functional', 'targeting'],
|
|
10
|
+
});
|
|
11
|
+
expect(getDefaultTrackingPreference(Region.EU, config)).toEqual({
|
|
12
|
+
region: Region.EU,
|
|
13
|
+
consent: ['necessary'],
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Config, Region, TrackingCategory, TrackingPreference } from '../types';
|
|
2
|
+
import isOptOut from './isOptOut';
|
|
3
|
+
|
|
4
|
+
const getDefaultTrackingPreference = (region: Region, config: Config): TrackingPreference => {
|
|
5
|
+
// In opt in regions, we only allow required trackers
|
|
6
|
+
|
|
7
|
+
if (!isOptOut(region)) {
|
|
8
|
+
const categories = config.categories
|
|
9
|
+
.map((c) => {
|
|
10
|
+
if (c.required) {
|
|
11
|
+
return c.id;
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
.filter((c) => !!c) as TrackingCategory[];
|
|
15
|
+
return { region, consent: categories };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const categories = config.categories
|
|
19
|
+
.map((c) => c.id)
|
|
20
|
+
.filter((id) => id !== TrackingCategory.DELETE_IF_SEEN);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
region,
|
|
24
|
+
consent: categories,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default getDefaultTrackingPreference;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getDomainWithoutSubdomain } from './getDomain';
|
|
2
|
+
|
|
3
|
+
describe('getDomain', () => {
|
|
4
|
+
it('returns the correct domain', () => {
|
|
5
|
+
Object.defineProperty(window, 'location', {
|
|
6
|
+
value: { hostname: 'www.coinbase.com' },
|
|
7
|
+
writable: true,
|
|
8
|
+
});
|
|
9
|
+
expect(getDomainWithoutSubdomain()).toEqual('.coinbase.com');
|
|
10
|
+
window.location.hostname = 'pro.coinbase.com';
|
|
11
|
+
expect(getDomainWithoutSubdomain()).toEqual('.coinbase.com');
|
|
12
|
+
window.location.hostname = 'coinbase-dev.cbhq.net';
|
|
13
|
+
|
|
14
|
+
expect(getDomainWithoutSubdomain()).toEqual('.cbhq.net');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function getHostname(): string | undefined {
|
|
2
|
+
if (typeof window !== 'undefined') {
|
|
3
|
+
return window.location.hostname;
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getDomainWithoutSubdomain(): string | undefined {
|
|
8
|
+
const hostname = getHostname();
|
|
9
|
+
if (!hostname) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (hostname === 'localhost') {
|
|
14
|
+
return hostname;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const [, ...domain] = hostname.split('.');
|
|
18
|
+
return `.${domain.join('.')}`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import config from '../examples/config';
|
|
2
|
+
import { TrackingCategory } from '../types';
|
|
3
|
+
import getTrackerCategory from './getTrackerCategory';
|
|
4
|
+
|
|
5
|
+
const exampleSimpleTrackers = {
|
|
6
|
+
locale: TrackingCategory.NECESSARY,
|
|
7
|
+
device_id: TrackingCategory.PERFORMANCE,
|
|
8
|
+
mode: TrackingCategory.FUNCTIONAL,
|
|
9
|
+
gclid: TrackingCategory.TARGETING,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const exampleDynamicTrackers = {
|
|
13
|
+
'idundefined.com': TrackingCategory.TARGETING,
|
|
14
|
+
id_ac7a5c3da45e3612b44543a702e42b01: TrackingCategory.TARGETING,
|
|
15
|
+
id_132e62b5953ce8d568137d5887b6b7ab_classic: TrackingCategory.TARGETING,
|
|
16
|
+
id_ac7a5c3da45e3612b44543a702e42b01_classic: TrackingCategory.TARGETING,
|
|
17
|
+
id_5e21e8a10d7c1553bc75ce1ff6d49a3f_realtime: TrackingCategory.TARGETING,
|
|
18
|
+
id_4031eff2d750cfe4385594be339acc56_realtime: TrackingCategory.TARGETING,
|
|
19
|
+
'id_4031eff2d750cfe4385594be339acc56_realtimecoinbase.com': TrackingCategory.TARGETING,
|
|
20
|
+
'id_132e62b5953ce8d568137d5887b6b7abcoinbase.com': TrackingCategory.TARGETING,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('getTrackerCategory', () => {
|
|
24
|
+
it('matches simple trackers via id', () => {
|
|
25
|
+
for (const [tracker, categoryId] of Object.entries(exampleSimpleTrackers)) {
|
|
26
|
+
expect(getTrackerCategory(tracker, config)?.id).toEqual(categoryId);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
it('matches dynamic trackers not matching ids via regex', () => {
|
|
30
|
+
for (const [tracker, categoryId] of Object.entries(exampleDynamicTrackers)) {
|
|
31
|
+
expect(getTrackerCategory(tracker, config)?.id).toEqual(categoryId);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Config, ConfigCategoryInfo } from '../types';
|
|
2
|
+
import trackerMatches from './trackerMatches';
|
|
3
|
+
|
|
4
|
+
const getTrackerCategory = (trackerId: string, config: Config): ConfigCategoryInfo | undefined => {
|
|
5
|
+
const category = config.categories.find((category) =>
|
|
6
|
+
category.trackers.find((tracker) => trackerMatches(tracker, trackerId))
|
|
7
|
+
);
|
|
8
|
+
return category;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default getTrackerCategory;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import config from '../examples/config';
|
|
2
|
+
import getTrackerInfo from './getTrackerInfo';
|
|
3
|
+
|
|
4
|
+
describe('getTrackerInfo', () => {
|
|
5
|
+
it('should return undefined if tracker is not found', () => {
|
|
6
|
+
expect(getTrackerInfo('unknown', config)).toBeUndefined();
|
|
7
|
+
});
|
|
8
|
+
it('should return tracker info if tracker is found', () => {
|
|
9
|
+
expect(getTrackerInfo('locale', config)).toEqual({ id: 'locale', type: 'cookie' });
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Config } from '../types';
|
|
2
|
+
import getTrackerCategory from './getTrackerCategory';
|
|
3
|
+
import trackerMatches from './trackerMatches';
|
|
4
|
+
|
|
5
|
+
const getTrackerInfo = (trackerId: string, config: Config) => {
|
|
6
|
+
const trackingCategory = getTrackerCategory(trackerId, config);
|
|
7
|
+
return trackingCategory?.trackers.find((tracker) => trackerMatches(tracker, trackerId));
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default getTrackerInfo;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { REQUIRED_COOKIE_MANAGER_COOKIES } from '../constants';
|
|
2
|
+
import config from '../examples/config';
|
|
3
|
+
import { Region, TrackingCategory, TrackingPreference } from '../types';
|
|
4
|
+
import hasConsent from './hasConsent';
|
|
5
|
+
|
|
6
|
+
describe('hasConsent', () => {
|
|
7
|
+
let preference: TrackingPreference;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
preference = {
|
|
10
|
+
region: Region.DEFAULT,
|
|
11
|
+
consent: [
|
|
12
|
+
TrackingCategory.NECESSARY,
|
|
13
|
+
TrackingCategory.PERFORMANCE,
|
|
14
|
+
TrackingCategory.FUNCTIONAL,
|
|
15
|
+
TrackingCategory.TARGETING,
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('defaults to false if cookie is uncategorized', () => {
|
|
21
|
+
expect(hasConsent('random_cookie', config, preference)).toEqual(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns true for required cookie', () => {
|
|
25
|
+
preference = {
|
|
26
|
+
region: Region.DEFAULT,
|
|
27
|
+
consent: [TrackingCategory.PERFORMANCE],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
expect(hasConsent('logged_in', config, preference)).toEqual(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns true for cookies that are required to function', () => {
|
|
34
|
+
preference = {
|
|
35
|
+
region: Region.DEFAULT,
|
|
36
|
+
consent: [TrackingCategory.PERFORMANCE],
|
|
37
|
+
};
|
|
38
|
+
REQUIRED_COOKIE_MANAGER_COOKIES.map((c) => {
|
|
39
|
+
expect(hasConsent(c, config, preference)).toEqual(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns the correct consent', () => {
|
|
44
|
+
preference = {
|
|
45
|
+
region: Region.DEFAULT,
|
|
46
|
+
consent: [TrackingCategory.PERFORMANCE],
|
|
47
|
+
};
|
|
48
|
+
// Default
|
|
49
|
+
expect(hasConsent('device_id', config, preference)).toEqual(true);
|
|
50
|
+
expect(hasConsent('locale', config, preference)).toEqual(true);
|
|
51
|
+
|
|
52
|
+
// EU
|
|
53
|
+
preference = {
|
|
54
|
+
region: Region.EU,
|
|
55
|
+
consent: [TrackingCategory.PERFORMANCE],
|
|
56
|
+
};
|
|
57
|
+
expect(hasConsent('device_id', config, preference)).toEqual(true);
|
|
58
|
+
preference = {
|
|
59
|
+
region: Region.EU,
|
|
60
|
+
consent: [TrackingCategory.FUNCTIONAL],
|
|
61
|
+
};
|
|
62
|
+
expect(hasConsent('g_clid', config, preference)).toEqual(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { REQUIRED_COOKIE_MANAGER_COOKIES } from '../constants';
|
|
2
|
+
import { Config, TrackingPreference } from '../types';
|
|
3
|
+
import getTrackerCategory from './getTrackerCategory';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Used for determining if a specific tracker has user consent.
|
|
7
|
+
* It follows the following logic in order:
|
|
8
|
+
* 1) If we don't have a tracker category in the config then dont allow
|
|
9
|
+
* 2) If cookie is in a required category then allow
|
|
10
|
+
* 3) If cookie is required for cookie manager to function then allow
|
|
11
|
+
* 4) Determine consent from user consent preferences
|
|
12
|
+
*/
|
|
13
|
+
const hasConsent = (
|
|
14
|
+
tracker: string,
|
|
15
|
+
config: Config,
|
|
16
|
+
trackingPreference: TrackingPreference
|
|
17
|
+
): boolean => {
|
|
18
|
+
const trackingCategory = getTrackerCategory(tracker, config);
|
|
19
|
+
|
|
20
|
+
if (REQUIRED_COOKIE_MANAGER_COOKIES.includes(tracker)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (!trackingCategory) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (trackingCategory.required) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return trackingPreference.consent.includes(trackingCategory.id);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default hasConsent;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import isMaxKBSize from './isMaxKBSize';
|
|
2
|
+
|
|
3
|
+
describe('isMaxKBSize', () => {
|
|
4
|
+
it('returns true when string exceeds max kb size', () => {
|
|
5
|
+
expect(isMaxKBSize('helloooooooo', 0.01)).toEqual(true);
|
|
6
|
+
});
|
|
7
|
+
it('returns false when string does not max kb size', () => {
|
|
8
|
+
expect(isMaxKBSize('hello', 0.01)).toEqual(false);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Region } from '../types';
|
|
2
|
+
import isOptOut from './isOptOut';
|
|
3
|
+
|
|
4
|
+
describe('isOptOut', () => {
|
|
5
|
+
it('uses default rule if there is no rule for a region', () => {
|
|
6
|
+
// @ts-expect-error we are deliberately passing a wrong value
|
|
7
|
+
expect(isOptOut('US')).toEqual(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns the correct value', () => {
|
|
11
|
+
expect(isOptOut(Region.EU)).toEqual(false);
|
|
12
|
+
expect(isOptOut(Region.DEFAULT)).toEqual(true);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { GEOLOCATION_RULES } from '../constants';
|
|
2
|
+
import { Framework, Region } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Used for determining the if current region is using the optOut framework
|
|
6
|
+
* It follows the following logic in order:
|
|
7
|
+
* 1) If there's no geolocation rule for the specified region
|
|
8
|
+
* then use the rule for DEFAULT region
|
|
9
|
+
* - Otherwise use the DEFAULT_FRAMEWORK
|
|
10
|
+
* 2) Use geolocation rule
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* optIn: Users must opt in to tracking (EU)
|
|
15
|
+
* optOut: Users must opt out of tracking (non-EU)
|
|
16
|
+
* We use the most restrictive framework (optIn) if we don't know the framework
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const isOptOut = (region: Region) => {
|
|
20
|
+
const rule = GEOLOCATION_RULES.find((r) => r.region === region);
|
|
21
|
+
|
|
22
|
+
if (!rule) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return rule.framework === Framework.OPT_OUT;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default isOptOut;
|