@guardian/commercial-core 0.29.0 → 0.33.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.
package/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
  [![Generic badge](https://img.shields.io/badge/google-chat-259082.svg)](https://chat.google.com/room/AAAAPL2MBvE)
25
25
 
26
26
  ```bash
27
- pnpm add @guardian/commercial-core
27
+ yarn add @guardian/commercial-core
28
28
  ```
29
29
 
30
30
  or
@@ -44,7 +44,7 @@ If your target environment does not support that, make sure you transpile this p
44
44
  ### Requirements
45
45
 
46
46
  1. [Node 14](https://nodejs.org/en/download/) ([nvm][] or [fnm][] recommended)
47
- 2. [PNPM](https://pnpm.io/installation)
47
+ 2. [Yarn](https://classic.yarnpkg.com/en/docs/install/)
48
48
 
49
49
  [nvm]: https://github.com/nvm-sh/nvm
50
50
  [fnm]: https://github.com/Schniz/fnm
@@ -37,10 +37,10 @@ export declare class EventTimer {
37
37
  effectiveType?: string;
38
38
  };
39
39
  /**
40
- * Initalise the EventTimer class on page.
40
+ * Initialise the EventTimer class on page.
41
41
  * Returns the singleton instance of the EventTimer class and binds
42
42
  * to window.guardian.commercialTimer. If it's been previously
43
- * initalised and bound it returns the original instance
43
+ * initialised and bound it returns the original instance
44
44
  * Note: We save to window.guardian.commercialTimer because
45
45
  * different bundles (DCR / DCP) can use commercial core, and we want
46
46
  * all timer events saved to a single instance per-page
@@ -58,7 +58,6 @@ export declare class EventTimer {
58
58
  */
59
59
  get events(): Event[];
60
60
  constructor();
61
- mark(name: string): void;
62
61
  /**
63
62
  * Creates a new performance mark
64
63
  * For slot events also ensures each TYPE of event event is marked only once for 'first'
@@ -68,6 +67,7 @@ export declare class EventTimer {
68
67
  * @param {origin} [origin=page] - Either 'page' (default) or the name of the slot
69
68
  */
70
69
  trigger(eventName: string, origin?: string): void;
71
- trackInGA(eventName: string, label?: string): void;
70
+ private mark;
71
+ private trackInGA;
72
72
  }
73
73
  export {};
@@ -66,10 +66,10 @@ class EventTimer {
66
66
  : {};
67
67
  }
68
68
  /**
69
- * Initalise the EventTimer class on page.
69
+ * Initialise the EventTimer class on page.
70
70
  * Returns the singleton instance of the EventTimer class and binds
71
71
  * to window.guardian.commercialTimer. If it's been previously
72
- * initalised and bound it returns the original instance
72
+ * initialised and bound it returns the original instance
73
73
  * Note: We save to window.guardian.commercialTimer because
74
74
  * different bundles (DCR / DCP) can use commercial core, and we want
75
75
  * all timer events saved to a single instance per-page
@@ -102,20 +102,6 @@ class EventTimer {
102
102
  ]
103
103
  : this._events;
104
104
  }
105
- mark(name) {
106
- const longName = `gu.commercial.${name}`;
107
- if (typeof window.performance !== 'undefined' &&
108
- 'mark' in window.performance) {
109
- window.performance.mark(longName);
110
- // Most recent mark with this name is the event we just created.
111
- const mark = window.performance
112
- .getEntriesByName(longName, 'mark')
113
- .slice(-1)[0];
114
- if (typeof mark !== 'undefined') {
115
- this._events.push(new Event(name, mark));
116
- }
117
- }
118
- }
119
105
  /**
120
106
  * Creates a new performance mark
121
107
  * For slot events also ensures each TYPE of event event is marked only once for 'first'
@@ -125,7 +111,7 @@ class EventTimer {
125
111
  * @param {origin} [origin=page] - Either 'page' (default) or the name of the slot
126
112
  */
127
113
  trigger(eventName, origin = 'page') {
128
- const TRACKEDSLOTNAME = 'top-above-nav';
114
+ const TRACKED_SLOT_NAME = 'top-above-nav';
129
115
  if (origin === 'page' &&
130
116
  !this.triggers.page[eventName]) {
131
117
  this.mark(eventName);
@@ -139,12 +125,26 @@ class EventTimer {
139
125
  this.trackInGA(eventName, trackLabel);
140
126
  this.triggers.first[eventName] = true;
141
127
  }
142
- if (origin === TRACKEDSLOTNAME) {
143
- if (!this.triggers[TRACKEDSLOTNAME][eventName]) {
144
- const trackLabel = `${TRACKEDSLOTNAME}-${eventName}`;
128
+ if (origin === TRACKED_SLOT_NAME) {
129
+ if (!this.triggers[TRACKED_SLOT_NAME][eventName]) {
130
+ const trackLabel = `${TRACKED_SLOT_NAME}-${eventName}`;
145
131
  this.mark(trackLabel);
146
132
  this.trackInGA(eventName, trackLabel);
147
- this.triggers[TRACKEDSLOTNAME][eventName] = true;
133
+ this.triggers[TRACKED_SLOT_NAME][eventName] = true;
134
+ }
135
+ }
136
+ }
137
+ mark(name) {
138
+ const longName = `gu.commercial.${name}`;
139
+ if (typeof window.performance !== 'undefined' &&
140
+ 'mark' in window.performance) {
141
+ window.performance.mark(longName);
142
+ // Most recent mark with this name is the event we just created.
143
+ const mark = window.performance
144
+ .getEntriesByName(longName, 'mark')
145
+ .slice(-1)[0];
146
+ if (typeof mark !== 'undefined') {
147
+ this._events.push(new Event(name, mark));
148
148
  }
149
149
  }
150
150
  }
@@ -159,9 +159,12 @@ class EventTimer {
159
159
  exports.EventTimer = EventTimer;
160
160
  EventTimer._externallyDefinedEventNames = [
161
161
  'cmp-tcfv2-init',
162
+ 'cmp-tcfv2-ui-displayed',
162
163
  'cmp-tcfv2-got-consent',
163
164
  'cmp-ccpa-init',
165
+ 'cmp-ccpa-ui-displayed',
164
166
  'cmp-ccpa-got-consent',
165
167
  'cmp-aus-init',
168
+ 'cmp-aus-ui-displayed',
166
169
  'cmp-aus-got-consent',
167
170
  ];
@@ -16,8 +16,9 @@ function adElementBlocked(ad) {
16
16
  ad.offsetTop === 0 ||
17
17
  ad.offsetWidth === 0 ||
18
18
  ad.clientHeight === 0 ||
19
- ad.clientWidth === 0)
19
+ ad.clientWidth === 0) {
20
20
  return true;
21
+ }
21
22
  const adStyles = window.getComputedStyle(ad);
22
23
  if (adStyles.getPropertyValue('display') === 'none')
23
24
  return true;
@@ -8,7 +8,7 @@ declare type AdManagerGroup = typeof adManagerGroups[number];
8
8
  * Personalised Targeting requires user consent
9
9
  *
10
10
  * It allows or prevents personalised advertising, restrict data processing
11
- * and handles access to cookies and localstorage
11
+ * and handles access to cookies and local storage
12
12
  */
13
13
  export declare type PersonalisedTargeting = {
14
14
  /**
@@ -0,0 +1,28 @@
1
+ declare type ValidTargeting<T, K> = '' extends T ? never : [] extends T ? never : [''] extends T ? never : T extends boolean ? never : T extends NonNullable<T> ? K : never;
2
+ declare type DefinedKeys<T> = {
3
+ [K in keyof T]-?: ValidTargeting<T[K], K>;
4
+ }[keyof T];
5
+ declare type ObjectWithDefinedValues<T> = Pick<T, DefinedKeys<T>>;
6
+ /**
7
+ * Picks only keys with targeting values from an object.
8
+ * A targeting values is defined as either:
9
+ * - a non-empty string
10
+ * - an array of non-empty strings
11
+ *
12
+ * If you object is read-only, you can safely access properties on the result.
13
+ * For example:
14
+ *
15
+ * ```ts
16
+ * dirty = {
17
+ * valid: 'real',
18
+ * invalid: undefined,
19
+ * } as const;
20
+ *
21
+ * clean = pickDefinedValues(dirty);
22
+ *
23
+ * // @ts-expect-error -- you can’t access this property
24
+ * clean.invalid
25
+ * ```
26
+ */
27
+ export declare const pickTargetingValues: <T extends Record<string, string | readonly string[] | undefined>>(obj: T) => ObjectWithDefinedValues<T>;
28
+ export {};
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pickTargetingValues = void 0;
4
+ const libs_1 = require("@guardian/libs");
5
+ const isTargetingString = (string) => (0, libs_1.isString)(string) && string !== '';
6
+ const isTargetingArray = (array) => Array.isArray(array) && array.filter(isTargetingString).length > 0;
7
+ const isValidTargeting = (value) => {
8
+ if (isTargetingString(value))
9
+ return true;
10
+ if (isTargetingArray(value))
11
+ return true;
12
+ return false;
13
+ };
14
+ /**
15
+ * Picks only keys with targeting values from an object.
16
+ * A targeting values is defined as either:
17
+ * - a non-empty string
18
+ * - an array of non-empty strings
19
+ *
20
+ * If you object is read-only, you can safely access properties on the result.
21
+ * For example:
22
+ *
23
+ * ```ts
24
+ * dirty = {
25
+ * valid: 'real',
26
+ * invalid: undefined,
27
+ * } as const;
28
+ *
29
+ * clean = pickDefinedValues(dirty);
30
+ *
31
+ * // @ts-expect-error -- you can’t access this property
32
+ * clean.invalid
33
+ * ```
34
+ */
35
+ const pickTargetingValues = (obj) => {
36
+ const initialValue = {};
37
+ return Object.entries(obj).reduce((valid, [key, value]) => {
38
+ if (isValidTargeting(value)) {
39
+ // @ts-expect-error -- isValidTargeting checks this
40
+ valid[key] = value;
41
+ }
42
+ return valid;
43
+ }, initialValue);
44
+ };
45
+ exports.pickTargetingValues = pickTargetingValues;
@@ -0,0 +1,98 @@
1
+ import type { Participations } from '@guardian/ab-core';
2
+ import type { CountryCode } from '@guardian/libs';
3
+ import type { False, True } from '../types';
4
+ declare const referrers: readonly [{
5
+ readonly id: "facebook";
6
+ readonly match: "facebook.com";
7
+ }, {
8
+ readonly id: "google";
9
+ readonly match: "www.google";
10
+ }, {
11
+ readonly id: "twitter";
12
+ readonly match: "/t.co/";
13
+ }, {
14
+ readonly id: "reddit";
15
+ readonly match: "reddit.com";
16
+ }];
17
+ /**
18
+ * Session Targeting is based on the browser session
19
+ *
20
+ * Includes information such as the country of origin, referrer, page view ID.
21
+ *
22
+ * These values identify a browser session are either generated client-side,
23
+ * read from a cookie or passed down from the server.
24
+ */
25
+ export declare type SessionTargeting = {
26
+ /**
27
+ * **AB** Tests – [see on Ad Manager][gam]
28
+ *
29
+ * Type: _Dynamic_
30
+ *
31
+ * Values: typically start with `ab`
32
+ *
33
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=186327
34
+ */
35
+ ab: string[] | null;
36
+ /**
37
+ * **A**d **T**est – [see on Ad Manager][gam]
38
+ *
39
+ * Used for testing purposes, based on query param and/or cookie.
40
+ *
41
+ * Type: _Dynamic_
42
+ *
43
+ * [See Current values](https://frontend.gutools.co.uk/commercial/adtests)
44
+ *
45
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=177567
46
+ */
47
+ at: string | null;
48
+ /**
49
+ * **C**ountry **C**ode – [see on Ad Manager][gam]
50
+ *
51
+ * Type: _Dynamic_
52
+ *
53
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=11703293
54
+ */
55
+ cc: CountryCode;
56
+ /**
57
+ * Ophan **P**age **V**iew id – [see on Ad Manager][gam]
58
+ *
59
+ * ID Generated client-side, usually available on
60
+ * `guardian.config.ophan.pageViewId`
61
+ *
62
+ * Used mainly for internal reporting
63
+ *
64
+ * Type: _Dynamic_
65
+ *
66
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=206127
67
+ */
68
+ pv: string;
69
+ /**
70
+ * **Ref**errer – [see on Ad Manager][gam]
71
+ *
72
+ * Type: _Dynamic_
73
+ *
74
+ * Sample values:
75
+ * - `facebook`
76
+ * - `google`
77
+ * - `googleplus`
78
+ * - `reddit`
79
+ * - `twitter`
80
+ *
81
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=228567
82
+ */
83
+ ref: typeof referrers[number]['id'] | null;
84
+ /**
85
+ * **S**igned **I**n – [see on Ad Manager][gam]
86
+ *
87
+ *Whether a user is signed in. Based on presence of a cookie.
88
+ *
89
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=215727
90
+ */
91
+ si: True | False;
92
+ };
93
+ export declare type AllParticipations = {
94
+ clientSideParticipations: Participations;
95
+ serverSideParticipations: Record<string, 'control' | 'variant'>;
96
+ };
97
+ export declare const getSessionTargeting: (referrer: string, participations: AllParticipations, targeting: Omit<SessionTargeting, 'ab' | 'ref'>) => SessionTargeting;
98
+ export {};
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getSessionTargeting = void 0;
4
+ const libs_1 = require("@guardian/libs");
5
+ /* -- Types -- */
6
+ const referrers = [
7
+ {
8
+ id: 'facebook',
9
+ match: 'facebook.com',
10
+ },
11
+ {
12
+ id: 'google',
13
+ match: 'www.google',
14
+ },
15
+ {
16
+ id: 'twitter',
17
+ match: '/t.co/',
18
+ },
19
+ {
20
+ id: 'reddit',
21
+ match: 'reddit.com',
22
+ },
23
+ ];
24
+ /* -- Methods -- */
25
+ const getReferrer = (referrer) => {
26
+ if (referrer === '')
27
+ return null;
28
+ const matchedRef = referrers.find((referrerType) => referrer.includes(referrerType.match)) ?? null;
29
+ return matchedRef ? matchedRef.id : null;
30
+ };
31
+ const experimentsTargeting = ({ clientSideParticipations, serverSideParticipations, }) => {
32
+ const testToParams = (testName, variant) => {
33
+ if (variant === 'notintest')
34
+ return null;
35
+ // GAM key-value pairs accept value strings up to 40 characters long
36
+ return `${testName}-${variant}`.substring(0, 40);
37
+ };
38
+ const clientSideExperiment = Object.entries(clientSideParticipations)
39
+ .map((test) => {
40
+ const [name, variant] = test;
41
+ return testToParams(name, variant.variant);
42
+ })
43
+ .filter(libs_1.isString);
44
+ const serverSideExperiments = Object.entries(serverSideParticipations)
45
+ .map((test) => testToParams(...test))
46
+ .filter(libs_1.isString);
47
+ if (clientSideExperiment.length + serverSideExperiments.length === 0) {
48
+ return null;
49
+ }
50
+ return [...clientSideExperiment, ...serverSideExperiments];
51
+ };
52
+ /* -- Targeting -- */
53
+ const getSessionTargeting = (referrer, participations, targeting) => ({
54
+ ref: getReferrer(referrer),
55
+ ab: experimentsTargeting(participations),
56
+ ...targeting,
57
+ });
58
+ exports.getSessionTargeting = getSessionTargeting;
@@ -1,10 +1,10 @@
1
- export declare type TagAtrribute = {
1
+ export declare type TagAttribute = {
2
2
  name: string;
3
3
  value: string;
4
4
  };
5
5
  export declare type ThirdPartyTag = {
6
6
  async?: boolean;
7
- attrs?: TagAtrribute[];
7
+ attrs?: TagAttribute[];
8
8
  beforeLoad?: () => void;
9
9
  insertSnippet?: () => void;
10
10
  loaded?: boolean;
@@ -37,10 +37,10 @@ export declare class EventTimer {
37
37
  effectiveType?: string;
38
38
  };
39
39
  /**
40
- * Initalise the EventTimer class on page.
40
+ * Initialise the EventTimer class on page.
41
41
  * Returns the singleton instance of the EventTimer class and binds
42
42
  * to window.guardian.commercialTimer. If it's been previously
43
- * initalised and bound it returns the original instance
43
+ * initialised and bound it returns the original instance
44
44
  * Note: We save to window.guardian.commercialTimer because
45
45
  * different bundles (DCR / DCP) can use commercial core, and we want
46
46
  * all timer events saved to a single instance per-page
@@ -58,7 +58,6 @@ export declare class EventTimer {
58
58
  */
59
59
  get events(): Event[];
60
60
  constructor();
61
- mark(name: string): void;
62
61
  /**
63
62
  * Creates a new performance mark
64
63
  * For slot events also ensures each TYPE of event event is marked only once for 'first'
@@ -68,6 +67,7 @@ export declare class EventTimer {
68
67
  * @param {origin} [origin=page] - Either 'page' (default) or the name of the slot
69
68
  */
70
69
  trigger(eventName: string, origin?: string): void;
71
- trackInGA(eventName: string, label?: string): void;
70
+ private mark;
71
+ private trackInGA;
72
72
  }
73
73
  export {};
@@ -63,10 +63,10 @@ export class EventTimer {
63
63
  : {};
64
64
  }
65
65
  /**
66
- * Initalise the EventTimer class on page.
66
+ * Initialise the EventTimer class on page.
67
67
  * Returns the singleton instance of the EventTimer class and binds
68
68
  * to window.guardian.commercialTimer. If it's been previously
69
- * initalised and bound it returns the original instance
69
+ * initialised and bound it returns the original instance
70
70
  * Note: We save to window.guardian.commercialTimer because
71
71
  * different bundles (DCR / DCP) can use commercial core, and we want
72
72
  * all timer events saved to a single instance per-page
@@ -99,20 +99,6 @@ export class EventTimer {
99
99
  ]
100
100
  : this._events;
101
101
  }
102
- mark(name) {
103
- const longName = `gu.commercial.${name}`;
104
- if (typeof window.performance !== 'undefined' &&
105
- 'mark' in window.performance) {
106
- window.performance.mark(longName);
107
- // Most recent mark with this name is the event we just created.
108
- const mark = window.performance
109
- .getEntriesByName(longName, 'mark')
110
- .slice(-1)[0];
111
- if (typeof mark !== 'undefined') {
112
- this._events.push(new Event(name, mark));
113
- }
114
- }
115
- }
116
102
  /**
117
103
  * Creates a new performance mark
118
104
  * For slot events also ensures each TYPE of event event is marked only once for 'first'
@@ -122,7 +108,7 @@ export class EventTimer {
122
108
  * @param {origin} [origin=page] - Either 'page' (default) or the name of the slot
123
109
  */
124
110
  trigger(eventName, origin = 'page') {
125
- const TRACKEDSLOTNAME = 'top-above-nav';
111
+ const TRACKED_SLOT_NAME = 'top-above-nav';
126
112
  if (origin === 'page' &&
127
113
  !this.triggers.page[eventName]) {
128
114
  this.mark(eventName);
@@ -136,12 +122,26 @@ export class EventTimer {
136
122
  this.trackInGA(eventName, trackLabel);
137
123
  this.triggers.first[eventName] = true;
138
124
  }
139
- if (origin === TRACKEDSLOTNAME) {
140
- if (!this.triggers[TRACKEDSLOTNAME][eventName]) {
141
- const trackLabel = `${TRACKEDSLOTNAME}-${eventName}`;
125
+ if (origin === TRACKED_SLOT_NAME) {
126
+ if (!this.triggers[TRACKED_SLOT_NAME][eventName]) {
127
+ const trackLabel = `${TRACKED_SLOT_NAME}-${eventName}`;
142
128
  this.mark(trackLabel);
143
129
  this.trackInGA(eventName, trackLabel);
144
- this.triggers[TRACKEDSLOTNAME][eventName] = true;
130
+ this.triggers[TRACKED_SLOT_NAME][eventName] = true;
131
+ }
132
+ }
133
+ }
134
+ mark(name) {
135
+ const longName = `gu.commercial.${name}`;
136
+ if (typeof window.performance !== 'undefined' &&
137
+ 'mark' in window.performance) {
138
+ window.performance.mark(longName);
139
+ // Most recent mark with this name is the event we just created.
140
+ const mark = window.performance
141
+ .getEntriesByName(longName, 'mark')
142
+ .slice(-1)[0];
143
+ if (typeof mark !== 'undefined') {
144
+ this._events.push(new Event(name, mark));
145
145
  }
146
146
  }
147
147
  }
@@ -155,9 +155,12 @@ export class EventTimer {
155
155
  }
156
156
  EventTimer._externallyDefinedEventNames = [
157
157
  'cmp-tcfv2-init',
158
+ 'cmp-tcfv2-ui-displayed',
158
159
  'cmp-tcfv2-got-consent',
159
160
  'cmp-ccpa-init',
161
+ 'cmp-ccpa-ui-displayed',
160
162
  'cmp-ccpa-got-consent',
161
163
  'cmp-aus-init',
164
+ 'cmp-aus-ui-displayed',
162
165
  'cmp-aus-got-consent',
163
166
  ];
@@ -13,8 +13,9 @@ function adElementBlocked(ad) {
13
13
  ad.offsetTop === 0 ||
14
14
  ad.offsetWidth === 0 ||
15
15
  ad.clientHeight === 0 ||
16
- ad.clientWidth === 0)
16
+ ad.clientWidth === 0) {
17
17
  return true;
18
+ }
18
19
  const adStyles = window.getComputedStyle(ad);
19
20
  if (adStyles.getPropertyValue('display') === 'none')
20
21
  return true;
@@ -8,7 +8,7 @@ declare type AdManagerGroup = typeof adManagerGroups[number];
8
8
  * Personalised Targeting requires user consent
9
9
  *
10
10
  * It allows or prevents personalised advertising, restrict data processing
11
- * and handles access to cookies and localstorage
11
+ * and handles access to cookies and local storage
12
12
  */
13
13
  export declare type PersonalisedTargeting = {
14
14
  /**
@@ -0,0 +1,28 @@
1
+ declare type ValidTargeting<T, K> = '' extends T ? never : [] extends T ? never : [''] extends T ? never : T extends boolean ? never : T extends NonNullable<T> ? K : never;
2
+ declare type DefinedKeys<T> = {
3
+ [K in keyof T]-?: ValidTargeting<T[K], K>;
4
+ }[keyof T];
5
+ declare type ObjectWithDefinedValues<T> = Pick<T, DefinedKeys<T>>;
6
+ /**
7
+ * Picks only keys with targeting values from an object.
8
+ * A targeting values is defined as either:
9
+ * - a non-empty string
10
+ * - an array of non-empty strings
11
+ *
12
+ * If you object is read-only, you can safely access properties on the result.
13
+ * For example:
14
+ *
15
+ * ```ts
16
+ * dirty = {
17
+ * valid: 'real',
18
+ * invalid: undefined,
19
+ * } as const;
20
+ *
21
+ * clean = pickDefinedValues(dirty);
22
+ *
23
+ * // @ts-expect-error -- you can’t access this property
24
+ * clean.invalid
25
+ * ```
26
+ */
27
+ export declare const pickTargetingValues: <T extends Record<string, string | readonly string[] | undefined>>(obj: T) => ObjectWithDefinedValues<T>;
28
+ export {};
@@ -0,0 +1,41 @@
1
+ import { isString } from '@guardian/libs';
2
+ const isTargetingString = (string) => isString(string) && string !== '';
3
+ const isTargetingArray = (array) => Array.isArray(array) && array.filter(isTargetingString).length > 0;
4
+ const isValidTargeting = (value) => {
5
+ if (isTargetingString(value))
6
+ return true;
7
+ if (isTargetingArray(value))
8
+ return true;
9
+ return false;
10
+ };
11
+ /**
12
+ * Picks only keys with targeting values from an object.
13
+ * A targeting values is defined as either:
14
+ * - a non-empty string
15
+ * - an array of non-empty strings
16
+ *
17
+ * If you object is read-only, you can safely access properties on the result.
18
+ * For example:
19
+ *
20
+ * ```ts
21
+ * dirty = {
22
+ * valid: 'real',
23
+ * invalid: undefined,
24
+ * } as const;
25
+ *
26
+ * clean = pickDefinedValues(dirty);
27
+ *
28
+ * // @ts-expect-error -- you can’t access this property
29
+ * clean.invalid
30
+ * ```
31
+ */
32
+ export const pickTargetingValues = (obj) => {
33
+ const initialValue = {};
34
+ return Object.entries(obj).reduce((valid, [key, value]) => {
35
+ if (isValidTargeting(value)) {
36
+ // @ts-expect-error -- isValidTargeting checks this
37
+ valid[key] = value;
38
+ }
39
+ return valid;
40
+ }, initialValue);
41
+ };
@@ -0,0 +1,98 @@
1
+ import type { Participations } from '@guardian/ab-core';
2
+ import type { CountryCode } from '@guardian/libs';
3
+ import type { False, True } from '../types';
4
+ declare const referrers: readonly [{
5
+ readonly id: "facebook";
6
+ readonly match: "facebook.com";
7
+ }, {
8
+ readonly id: "google";
9
+ readonly match: "www.google";
10
+ }, {
11
+ readonly id: "twitter";
12
+ readonly match: "/t.co/";
13
+ }, {
14
+ readonly id: "reddit";
15
+ readonly match: "reddit.com";
16
+ }];
17
+ /**
18
+ * Session Targeting is based on the browser session
19
+ *
20
+ * Includes information such as the country of origin, referrer, page view ID.
21
+ *
22
+ * These values identify a browser session are either generated client-side,
23
+ * read from a cookie or passed down from the server.
24
+ */
25
+ export declare type SessionTargeting = {
26
+ /**
27
+ * **AB** Tests – [see on Ad Manager][gam]
28
+ *
29
+ * Type: _Dynamic_
30
+ *
31
+ * Values: typically start with `ab`
32
+ *
33
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=186327
34
+ */
35
+ ab: string[] | null;
36
+ /**
37
+ * **A**d **T**est – [see on Ad Manager][gam]
38
+ *
39
+ * Used for testing purposes, based on query param and/or cookie.
40
+ *
41
+ * Type: _Dynamic_
42
+ *
43
+ * [See Current values](https://frontend.gutools.co.uk/commercial/adtests)
44
+ *
45
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=177567
46
+ */
47
+ at: string | null;
48
+ /**
49
+ * **C**ountry **C**ode – [see on Ad Manager][gam]
50
+ *
51
+ * Type: _Dynamic_
52
+ *
53
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=11703293
54
+ */
55
+ cc: CountryCode;
56
+ /**
57
+ * Ophan **P**age **V**iew id – [see on Ad Manager][gam]
58
+ *
59
+ * ID Generated client-side, usually available on
60
+ * `guardian.config.ophan.pageViewId`
61
+ *
62
+ * Used mainly for internal reporting
63
+ *
64
+ * Type: _Dynamic_
65
+ *
66
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=206127
67
+ */
68
+ pv: string;
69
+ /**
70
+ * **Ref**errer – [see on Ad Manager][gam]
71
+ *
72
+ * Type: _Dynamic_
73
+ *
74
+ * Sample values:
75
+ * - `facebook`
76
+ * - `google`
77
+ * - `googleplus`
78
+ * - `reddit`
79
+ * - `twitter`
80
+ *
81
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=228567
82
+ */
83
+ ref: typeof referrers[number]['id'] | null;
84
+ /**
85
+ * **S**igned **I**n – [see on Ad Manager][gam]
86
+ *
87
+ *Whether a user is signed in. Based on presence of a cookie.
88
+ *
89
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=215727
90
+ */
91
+ si: True | False;
92
+ };
93
+ export declare type AllParticipations = {
94
+ clientSideParticipations: Participations;
95
+ serverSideParticipations: Record<string, 'control' | 'variant'>;
96
+ };
97
+ export declare const getSessionTargeting: (referrer: string, participations: AllParticipations, targeting: Omit<SessionTargeting, 'ab' | 'ref'>) => SessionTargeting;
98
+ export {};
@@ -0,0 +1,54 @@
1
+ import { isString } from '@guardian/libs';
2
+ /* -- Types -- */
3
+ const referrers = [
4
+ {
5
+ id: 'facebook',
6
+ match: 'facebook.com',
7
+ },
8
+ {
9
+ id: 'google',
10
+ match: 'www.google',
11
+ },
12
+ {
13
+ id: 'twitter',
14
+ match: '/t.co/',
15
+ },
16
+ {
17
+ id: 'reddit',
18
+ match: 'reddit.com',
19
+ },
20
+ ];
21
+ /* -- Methods -- */
22
+ const getReferrer = (referrer) => {
23
+ if (referrer === '')
24
+ return null;
25
+ const matchedRef = referrers.find((referrerType) => referrer.includes(referrerType.match)) ?? null;
26
+ return matchedRef ? matchedRef.id : null;
27
+ };
28
+ const experimentsTargeting = ({ clientSideParticipations, serverSideParticipations, }) => {
29
+ const testToParams = (testName, variant) => {
30
+ if (variant === 'notintest')
31
+ return null;
32
+ // GAM key-value pairs accept value strings up to 40 characters long
33
+ return `${testName}-${variant}`.substring(0, 40);
34
+ };
35
+ const clientSideExperiment = Object.entries(clientSideParticipations)
36
+ .map((test) => {
37
+ const [name, variant] = test;
38
+ return testToParams(name, variant.variant);
39
+ })
40
+ .filter(isString);
41
+ const serverSideExperiments = Object.entries(serverSideParticipations)
42
+ .map((test) => testToParams(...test))
43
+ .filter(isString);
44
+ if (clientSideExperiment.length + serverSideExperiments.length === 0) {
45
+ return null;
46
+ }
47
+ return [...clientSideExperiment, ...serverSideExperiments];
48
+ };
49
+ /* -- Targeting -- */
50
+ export const getSessionTargeting = (referrer, participations, targeting) => ({
51
+ ref: getReferrer(referrer),
52
+ ab: experimentsTargeting(participations),
53
+ ...targeting,
54
+ });
@@ -1,10 +1,10 @@
1
- export declare type TagAtrribute = {
1
+ export declare type TagAttribute = {
2
2
  name: string;
3
3
  value: string;
4
4
  };
5
5
  export declare type ThirdPartyTag = {
6
6
  async?: boolean;
7
- attrs?: TagAtrribute[];
7
+ attrs?: TagAttribute[];
8
8
  beforeLoad?: () => void;
9
9
  insertSnippet?: () => void;
10
10
  loaded?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guardian/commercial-core",
3
- "version": "0.29.0",
3
+ "version": "0.33.0",
4
4
  "description": "Guardian advertising business logic",
5
5
  "homepage": "https://github.com/guardian/commercial-core#readme",
6
6
  "bugs": {
@@ -41,42 +41,44 @@
41
41
  },
42
42
  "prettier": "@guardian/prettier",
43
43
  "devDependencies": {
44
- "@commitlint/cli": "^13",
45
- "@commitlint/config-conventional": "^14",
46
- "@guardian/consent-management-platform": "^8",
47
- "@guardian/eslint-config-typescript": "^0.7",
48
- "@guardian/libs": "3.3.0",
49
- "@guardian/prettier": "^0.7",
50
- "@octokit/core": "^3",
51
- "@semantic-release/github": "^8",
52
- "@types/googletag": "^1.1.3",
44
+ "@commitlint/cli": "^15.0.0",
45
+ "@commitlint/config-conventional": "^15.0.0",
46
+ "@guardian/ab-core": "^2.0.0",
47
+ "@guardian/consent-management-platform": "^8.0.1",
48
+ "@guardian/eslint-config-typescript": "^0.7.0",
49
+ "@guardian/libs": "3.5.1",
50
+ "@guardian/prettier": "^0.7.0",
51
+ "@octokit/core": "^3.5.1",
52
+ "@semantic-release/github": "^8.0.2",
53
53
  "@types/google.analytics": "^0.0.42",
54
- "@types/jest": "^27.0.2",
55
- "@typescript-eslint/eslint-plugin": "^4.33.0",
56
- "@typescript-eslint/parser": "^4.33.0",
54
+ "@types/googletag": "^1.1.4",
55
+ "@types/jest": "^27.0.3",
56
+ "@typescript-eslint/eslint-plugin": "^5.5.0",
57
+ "@typescript-eslint/parser": "^5.5.0",
57
58
  "commitizen": "^4.2.4",
58
59
  "cz-conventional-changelog": "^3.3.0",
59
- "eslint": "^7.32.0",
60
+ "eslint": "^8.4.1",
60
61
  "eslint-config-prettier": "^8.3.0",
61
62
  "eslint-plugin-eslint-comments": "^3.2.0",
62
- "eslint-plugin-import": "^2.25.2",
63
- "eslint-plugin-jest": "^25.2.2",
63
+ "eslint-plugin-import": "^2.25.3",
64
+ "eslint-plugin-jest": "^25.3.0",
64
65
  "eslint-plugin-prettier": "^4.0.0",
65
66
  "husky": "^7.0.4",
66
- "jest": "^27.3.1",
67
- "lint-staged": "^11.2.6",
67
+ "jest": "^27.4.1",
68
+ "lint-staged": "^12.1.2",
68
69
  "mockdate": "^3.0.5",
69
70
  "npm-run-all": "^4.1.5",
70
- "prettier": "^2.4.1",
71
- "semantic-release": "^18.0.0",
71
+ "prettier": "^2.5.0",
72
+ "semantic-release": "^18.0.1",
72
73
  "ts-jest": "^27.0.7",
73
- "typescript": "^4.4.4",
74
+ "typescript": "^4.5.2",
74
75
  "web-vitals": "^2.1.2"
75
76
  },
76
77
  "publishConfig": {
77
78
  "access": "public"
78
79
  },
79
80
  "peerDependencies": {
81
+ "@guardian/ab-core": "^2.0.0",
80
82
  "@guardian/libs": "^3.3.0"
81
83
  }
82
84
  }