@guardian/commercial-core 0.0.0-beta-20250716121613

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.
Files changed (111) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +45 -0
  3. package/dist/cjs/ad-sizes.d.ts +202 -0
  4. package/dist/cjs/ad-sizes.js +400 -0
  5. package/dist/cjs/breakpoint.d.ts +8 -0
  6. package/dist/cjs/breakpoint.js +10 -0
  7. package/dist/cjs/constants/ad-label-height.d.ts +4 -0
  8. package/dist/cjs/constants/ad-label-height.js +7 -0
  9. package/dist/cjs/constants/index.d.ts +3 -0
  10. package/dist/cjs/constants/index.js +9 -0
  11. package/dist/cjs/constants/prebid-timeout.d.ts +4 -0
  12. package/dist/cjs/constants/prebid-timeout.js +7 -0
  13. package/dist/cjs/constants/top-above-nav-height.d.ts +10 -0
  14. package/dist/cjs/constants/top-above-nav-height.js +48 -0
  15. package/dist/cjs/detect-ad-blocker.d.ts +12 -0
  16. package/dist/cjs/detect-ad-blocker.js +61 -0
  17. package/dist/cjs/event-timer.d.ts +103 -0
  18. package/dist/cjs/event-timer.js +204 -0
  19. package/dist/cjs/geo/country-code.d.ts +3 -0
  20. package/dist/cjs/geo/country-code.js +34 -0
  21. package/dist/cjs/geo/geo-utils.d.ts +11 -0
  22. package/dist/cjs/geo/geo-utils.js +31 -0
  23. package/dist/cjs/geo/get-locale.d.ts +8 -0
  24. package/dist/cjs/geo/get-locale.js +43 -0
  25. package/dist/cjs/global.d.ts +71 -0
  26. package/dist/cjs/global.js +2 -0
  27. package/dist/cjs/index.d.ts +13 -0
  28. package/dist/cjs/index.js +46 -0
  29. package/dist/cjs/messenger/post-message.d.ts +1 -0
  30. package/dist/cjs/messenger/post-message.js +7 -0
  31. package/dist/cjs/permutive.d.ts +9 -0
  32. package/dist/cjs/permutive.js +38 -0
  33. package/dist/cjs/send-commercial-metrics.d.ts +58 -0
  34. package/dist/cjs/send-commercial-metrics.js +209 -0
  35. package/dist/cjs/targeting/build-page-targeting.d.ts +47 -0
  36. package/dist/cjs/targeting/build-page-targeting.js +112 -0
  37. package/dist/cjs/targeting/content.d.ts +87 -0
  38. package/dist/cjs/targeting/content.js +76 -0
  39. package/dist/cjs/targeting/personalised.d.ts +83 -0
  40. package/dist/cjs/targeting/personalised.js +140 -0
  41. package/dist/cjs/targeting/pick-targeting-values.d.ts +25 -0
  42. package/dist/cjs/targeting/pick-targeting-values.js +47 -0
  43. package/dist/cjs/targeting/session.d.ts +111 -0
  44. package/dist/cjs/targeting/session.js +61 -0
  45. package/dist/cjs/targeting/shared.d.ts +156 -0
  46. package/dist/cjs/targeting/shared.js +28 -0
  47. package/dist/cjs/targeting/teads-eligibility.d.ts +2 -0
  48. package/dist/cjs/targeting/teads-eligibility.js +20 -0
  49. package/dist/cjs/targeting/types.d.ts +6 -0
  50. package/dist/cjs/targeting/types.js +2 -0
  51. package/dist/cjs/targeting/viewport.d.ts +48 -0
  52. package/dist/cjs/targeting/viewport.js +22 -0
  53. package/dist/cjs/targeting/youtube-ima.d.ts +12 -0
  54. package/dist/cjs/targeting/youtube-ima.js +76 -0
  55. package/dist/cjs/types.d.ts +426 -0
  56. package/dist/cjs/types.js +12 -0
  57. package/dist/esm/ad-sizes.d.ts +202 -0
  58. package/dist/esm/ad-sizes.js +390 -0
  59. package/dist/esm/breakpoint.d.ts +8 -0
  60. package/dist/esm/breakpoint.js +6 -0
  61. package/dist/esm/constants/ad-label-height.d.ts +4 -0
  62. package/dist/esm/constants/ad-label-height.js +4 -0
  63. package/dist/esm/constants/index.d.ts +3 -0
  64. package/dist/esm/constants/index.js +3 -0
  65. package/dist/esm/constants/prebid-timeout.d.ts +4 -0
  66. package/dist/esm/constants/prebid-timeout.js +4 -0
  67. package/dist/esm/constants/top-above-nav-height.d.ts +10 -0
  68. package/dist/esm/constants/top-above-nav-height.js +45 -0
  69. package/dist/esm/detect-ad-blocker.d.ts +12 -0
  70. package/dist/esm/detect-ad-blocker.js +58 -0
  71. package/dist/esm/event-timer.d.ts +103 -0
  72. package/dist/esm/event-timer.js +199 -0
  73. package/dist/esm/geo/country-code.d.ts +3 -0
  74. package/dist/esm/geo/country-code.js +31 -0
  75. package/dist/esm/geo/geo-utils.d.ts +11 -0
  76. package/dist/esm/geo/geo-utils.js +20 -0
  77. package/dist/esm/geo/get-locale.d.ts +8 -0
  78. package/dist/esm/geo/get-locale.js +38 -0
  79. package/dist/esm/global.d.ts +71 -0
  80. package/dist/esm/global.js +0 -0
  81. package/dist/esm/index.d.ts +13 -0
  82. package/dist/esm/index.js +10 -0
  83. package/dist/esm/messenger/post-message.d.ts +1 -0
  84. package/dist/esm/messenger/post-message.js +3 -0
  85. package/dist/esm/permutive.d.ts +9 -0
  86. package/dist/esm/permutive.js +33 -0
  87. package/dist/esm/send-commercial-metrics.d.ts +58 -0
  88. package/dist/esm/send-commercial-metrics.js +204 -0
  89. package/dist/esm/targeting/build-page-targeting.d.ts +47 -0
  90. package/dist/esm/targeting/build-page-targeting.js +108 -0
  91. package/dist/esm/targeting/content.d.ts +87 -0
  92. package/dist/esm/targeting/content.js +73 -0
  93. package/dist/esm/targeting/personalised.d.ts +83 -0
  94. package/dist/esm/targeting/personalised.js +137 -0
  95. package/dist/esm/targeting/pick-targeting-values.d.ts +25 -0
  96. package/dist/esm/targeting/pick-targeting-values.js +43 -0
  97. package/dist/esm/targeting/session.d.ts +111 -0
  98. package/dist/esm/targeting/session.js +57 -0
  99. package/dist/esm/targeting/shared.d.ts +156 -0
  100. package/dist/esm/targeting/shared.js +25 -0
  101. package/dist/esm/targeting/teads-eligibility.d.ts +2 -0
  102. package/dist/esm/targeting/teads-eligibility.js +17 -0
  103. package/dist/esm/targeting/types.d.ts +6 -0
  104. package/dist/esm/targeting/types.js +0 -0
  105. package/dist/esm/targeting/viewport.d.ts +48 -0
  106. package/dist/esm/targeting/viewport.js +19 -0
  107. package/dist/esm/targeting/youtube-ima.d.ts +12 -0
  108. package/dist/esm/targeting/youtube-ima.js +73 -0
  109. package/dist/esm/types.d.ts +426 -0
  110. package/dist/esm/types.js +10 -0
  111. package/package.json +65 -0
@@ -0,0 +1,58 @@
1
+ import type { ConnectionType } from './types';
2
+ type Metric = {
3
+ name: string;
4
+ value: number;
5
+ };
6
+ type Property = {
7
+ name: string;
8
+ value: string;
9
+ };
10
+ type TimedEvent = {
11
+ name: string;
12
+ ts: number;
13
+ };
14
+ type DurationEvent = {
15
+ name: string;
16
+ duration: number;
17
+ };
18
+ type EventProperties = {
19
+ type?: ConnectionType;
20
+ downlink?: number;
21
+ effectiveType?: string;
22
+ };
23
+ declare enum Endpoints {
24
+ CODE = "//performance-events.code.dev-guardianapis.com/commercial-metrics",
25
+ PROD = "//performance-events.guardianapis.com/commercial-metrics"
26
+ }
27
+ declare const checkConsent: () => Promise<boolean>;
28
+ /**
29
+ * A method to asynchronously send metrics after initialization.
30
+ */
31
+ declare function bypassCommercialMetricsSampling(): Promise<void>;
32
+ interface InitCommercialMetricsArgs {
33
+ pageViewId: string;
34
+ browserId: string | undefined;
35
+ isDev: boolean;
36
+ adBlockerInUse?: boolean;
37
+ sampling?: number;
38
+ }
39
+ /**
40
+ * A method to initialise metrics.
41
+ * Note: this is initialised in the frontend/DCR bundles, not the commercial bundle.
42
+ * @param init.pageViewId - identifies the page view. Usually available on `guardian.config.ophan.pageViewId`. Defaults to `null`
43
+ * @param init.browserId - identifies the browser. Usually available via `getCookie({ name: 'bwid' })`. Defaults to `null`
44
+ * @param init.isDev - used to determine whether to use CODE or PROD endpoints.
45
+ * @param init.adBlockerInUse - indicates whether or not an adblocker is being used.
46
+ * @param init.sampling - rate at which to sample commercial metrics - the default is to send for 1% of pageviews
47
+ */
48
+ declare function initCommercialMetrics({ pageViewId, browserId, isDev, adBlockerInUse, sampling, }: InitCommercialMetricsArgs): Promise<boolean>;
49
+ export declare const _: {
50
+ Endpoints: typeof Endpoints;
51
+ setEndpoint: (isDev: boolean) => Endpoints;
52
+ mapEventTimerPropertiesToString: (properties: Array<[string, string | number]>) => Property[];
53
+ roundTimeStamp: (events: TimedEvent[], measures: DurationEvent[]) => Metric[];
54
+ transformToObjectEntries: (eventTimerProperties: EventProperties) => Array<[string, string | number | undefined]>;
55
+ reset: () => void;
56
+ };
57
+ export type { Property, TimedEvent, Metric };
58
+ export { bypassCommercialMetricsSampling, initCommercialMetrics, checkConsent };
@@ -0,0 +1,204 @@
1
+ import { getMeasures, isNonNullable, log, onConsent } from '@guardian/libs';
2
+ import { EventTimer } from './event-timer';
3
+ var Endpoints;
4
+ (function (Endpoints) {
5
+ Endpoints["CODE"] = "//performance-events.code.dev-guardianapis.com/commercial-metrics";
6
+ Endpoints["PROD"] = "//performance-events.guardianapis.com/commercial-metrics";
7
+ })(Endpoints || (Endpoints = {}));
8
+ let commercialMetricsPayload = {
9
+ page_view_id: undefined,
10
+ browser_id: undefined,
11
+ platform: 'NEXT_GEN',
12
+ metrics: [],
13
+ properties: [],
14
+ };
15
+ let devProperties = [];
16
+ let adBlockerProperties = [];
17
+ let endpoint;
18
+ const setEndpoint = (isDev) => (endpoint = isDev ? Endpoints.CODE : Endpoints.PROD);
19
+ const setDevProperties = (isDev) => (devProperties = isDev
20
+ ? [{ name: 'isDev', value: window.location.hostname }]
21
+ : []);
22
+ const setAdBlockerProperties = (adBlockerInUse) => {
23
+ adBlockerProperties =
24
+ adBlockerInUse !== undefined
25
+ ? [
26
+ {
27
+ name: 'adBlockerInUse',
28
+ value: adBlockerInUse.toString(),
29
+ },
30
+ ]
31
+ : [];
32
+ };
33
+ const transformToObjectEntries = (eventTimerProperties) => {
34
+ // Transforms object {key: value} pairs into an array of [key, value] arrays
35
+ return Object.entries(eventTimerProperties);
36
+ };
37
+ const mapEventTimerPropertiesToString = (properties) => {
38
+ return properties.map(([name, value]) => ({
39
+ name: String(name),
40
+ value: String(value),
41
+ }));
42
+ };
43
+ const roundTimeStamp = (events, measures) => {
44
+ const roundedEvents = events.map(({ name, ts }) => ({
45
+ name,
46
+ value: Math.ceil(ts),
47
+ }));
48
+ const roundedMeasures = measures.map(({ name, duration }) => ({
49
+ name,
50
+ value: Math.ceil(duration),
51
+ }));
52
+ return [...roundedEvents, ...roundedMeasures];
53
+ };
54
+ function sendMetrics() {
55
+ log('commercial', 'About to send commercial metrics', commercialMetricsPayload);
56
+ void fetch(endpoint, {
57
+ method: 'POST',
58
+ body: JSON.stringify(commercialMetricsPayload),
59
+ keepalive: true,
60
+ cache: 'no-store',
61
+ mode: 'no-cors',
62
+ });
63
+ }
64
+ /**
65
+ * Gather how many times the user has experienced the “offline” event
66
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event
67
+ *
68
+ * This value should be fetched as late as possible in the page lifecycle,
69
+ * to get an accurate value.
70
+ *
71
+ * Relevant for an @guardian/open-journalism investigation.
72
+ */
73
+ const getOfflineCount = () => typeof window.guardian.offlineCount === 'number'
74
+ ? [
75
+ {
76
+ name: 'offlineCount',
77
+ value: window.guardian.offlineCount,
78
+ },
79
+ ]
80
+ : [];
81
+ /**
82
+ * Measures added with @guardian/libs’s `startPerformanceMeasure`
83
+ *
84
+ * Allows for more granular monitoring of web page performance.
85
+ */
86
+ const getPerformanceMeasures = (...teams) => getMeasures(teams).map(({ detail: { subscription, name, action }, duration }) => ({
87
+ name: [subscription, name, action].filter(isNonNullable).join('_'),
88
+ value: duration,
89
+ }));
90
+ function gatherMetricsOnPageUnload() {
91
+ // Assemble commercial properties and metrics
92
+ const eventTimer = EventTimer.get();
93
+ const transformedEntries = transformToObjectEntries(eventTimer.properties);
94
+ const filteredEventTimerProperties = transformedEntries.filter((item) => typeof item[1] !== 'undefined');
95
+ const mappedEventTimerProperties = mapEventTimerPropertiesToString(filteredEventTimerProperties);
96
+ const properties = mappedEventTimerProperties
97
+ .concat(devProperties)
98
+ .concat(adBlockerProperties);
99
+ commercialMetricsPayload.properties = properties;
100
+ const metrics = roundTimeStamp(eventTimer.marks, eventTimer.measures)
101
+ .concat(getOfflineCount())
102
+ .concat(getPerformanceMeasures('dotcom'));
103
+ commercialMetricsPayload.metrics = metrics;
104
+ sendMetrics();
105
+ }
106
+ const listener = (e) => {
107
+ if (window.guardian.config.shouldSendCommercialMetrics) {
108
+ switch (e.type) {
109
+ case 'visibilitychange':
110
+ if (document.visibilityState === 'hidden') {
111
+ gatherMetricsOnPageUnload();
112
+ }
113
+ return;
114
+ case 'pagehide':
115
+ gatherMetricsOnPageUnload();
116
+ return;
117
+ }
118
+ }
119
+ };
120
+ const addVisibilityListeners = () => {
121
+ // Report all available metrics when the page is unloaded or in background.
122
+ window.addEventListener('visibilitychange', listener, { once: true });
123
+ // Safari does not reliably fire the `visibilitychange` on page unload.
124
+ window.addEventListener('pagehide', listener, { once: true });
125
+ };
126
+ const checkConsent = async () => {
127
+ const consentState = await onConsent();
128
+ if (consentState.tcfv2) {
129
+ // TCFv2 mode - check for consent
130
+ const consents = consentState.tcfv2.consents;
131
+ const REQUIRED_CONSENTS = [7, 8];
132
+ return REQUIRED_CONSENTS.every((consent) => consents[consent]);
133
+ }
134
+ // non-TCFv2 mode - don't check for consent
135
+ return true;
136
+ };
137
+ /**
138
+ * A method to asynchronously send metrics after initialization.
139
+ */
140
+ async function bypassCommercialMetricsSampling() {
141
+ if (!window.guardian.config.commercialMetricsInitialised) {
142
+ console.warn('initCommercialMetrics not yet initialised');
143
+ return;
144
+ }
145
+ const consented = await checkConsent();
146
+ if (consented) {
147
+ window.guardian.config.shouldSendCommercialMetrics = true;
148
+ }
149
+ else {
150
+ log('commercial', "Metrics won't be sent because consent wasn't given");
151
+ }
152
+ }
153
+ /**
154
+ * A method to initialise metrics.
155
+ * Note: this is initialised in the frontend/DCR bundles, not the commercial bundle.
156
+ * @param init.pageViewId - identifies the page view. Usually available on `guardian.config.ophan.pageViewId`. Defaults to `null`
157
+ * @param init.browserId - identifies the browser. Usually available via `getCookie({ name: 'bwid' })`. Defaults to `null`
158
+ * @param init.isDev - used to determine whether to use CODE or PROD endpoints.
159
+ * @param init.adBlockerInUse - indicates whether or not an adblocker is being used.
160
+ * @param init.sampling - rate at which to sample commercial metrics - the default is to send for 1% of pageviews
161
+ */
162
+ async function initCommercialMetrics({ pageViewId, browserId, isDev, adBlockerInUse, sampling = 1 / 100, }) {
163
+ commercialMetricsPayload.page_view_id = pageViewId;
164
+ commercialMetricsPayload.browser_id = browserId;
165
+ setEndpoint(isDev);
166
+ setDevProperties(isDev);
167
+ setAdBlockerProperties(adBlockerInUse);
168
+ addVisibilityListeners();
169
+ if (window.guardian.config.commercialMetricsInitialised) {
170
+ return false;
171
+ }
172
+ window.guardian.config.commercialMetricsInitialised = true;
173
+ const userIsInSamplingGroup = Math.random() <= sampling;
174
+ if (isDev || userIsInSamplingGroup) {
175
+ const consented = await checkConsent();
176
+ if (consented) {
177
+ window.guardian.config.shouldSendCommercialMetrics = true;
178
+ return true;
179
+ }
180
+ log('commercial', "Metrics won't be sent because consent wasn't given");
181
+ }
182
+ return false;
183
+ }
184
+ export const _ = {
185
+ Endpoints,
186
+ setEndpoint,
187
+ mapEventTimerPropertiesToString,
188
+ roundTimeStamp,
189
+ transformToObjectEntries,
190
+ reset: () => {
191
+ window.guardian.config.commercialMetricsInitialised = false;
192
+ window.guardian.config.shouldSendCommercialMetrics = false;
193
+ commercialMetricsPayload = {
194
+ page_view_id: undefined,
195
+ browser_id: undefined,
196
+ platform: 'NEXT_GEN',
197
+ metrics: [],
198
+ properties: [],
199
+ };
200
+ removeEventListener('visibilitychange', listener);
201
+ removeEventListener('pagehide', listener);
202
+ },
203
+ };
204
+ export { bypassCommercialMetricsSampling, initCommercialMetrics, checkConsent };
@@ -0,0 +1,47 @@
1
+ import type { Participations } from '@guardian/ab-core';
2
+ import type { ConsentState, CountryCode } from '@guardian/libs';
3
+ import type { AdManagerGroup, Frequency } from './personalised';
4
+ import type { SharedTargeting } from './shared';
5
+ import type { TrueOrFalse } from './types';
6
+ type PartialWithNulls<T> = {
7
+ [P in keyof T]?: T[P] | null;
8
+ };
9
+ type PageTargeting = PartialWithNulls<{
10
+ ab: string[];
11
+ af: 't';
12
+ amtgrp: AdManagerGroup;
13
+ at: string;
14
+ bp: 'mobile' | 'tablet' | 'desktop';
15
+ cc: CountryCode;
16
+ cmp_interaction: string;
17
+ consent_tcfv2: string;
18
+ dcre: TrueOrFalse;
19
+ fr: Frequency;
20
+ inskin: TrueOrFalse;
21
+ pa: TrueOrFalse;
22
+ permutive: string[];
23
+ pv: string;
24
+ rc: string;
25
+ rdp: string;
26
+ ref: string;
27
+ rp: 'dotcom-rendering' | 'dotcom-platform';
28
+ s: string;
29
+ sens: TrueOrFalse;
30
+ si: TrueOrFalse;
31
+ skinsize: 'l' | 's';
32
+ urlkw: string[];
33
+ vl: string;
34
+ allkw: string[];
35
+ [_: string]: string | string[];
36
+ } & SharedTargeting>;
37
+ declare const filterValues: (pageTargets: Record<string, unknown>) => Record<string, string | string[]>;
38
+ type BuildPageTargetingParams = {
39
+ adFree: boolean;
40
+ clientSideParticipations: Participations;
41
+ consentState: ConsentState;
42
+ isSignedIn?: boolean;
43
+ youtube?: boolean;
44
+ };
45
+ declare const buildPageTargeting: ({ adFree, clientSideParticipations, consentState, isSignedIn, youtube, }: BuildPageTargetingParams) => Record<string, string | string[]>;
46
+ export { buildPageTargeting, filterValues };
47
+ export type { PageTargeting };
@@ -0,0 +1,108 @@
1
+ import { cmp, getConsentFor, getCookie, isString } from '@guardian/libs';
2
+ import { supportsPerformanceAPI } from '../event-timer';
3
+ import { getLocale } from '../geo/get-locale';
4
+ import { getContentTargeting } from './content';
5
+ import { getPersonalisedTargeting } from './personalised';
6
+ import { getSessionTargeting } from './session';
7
+ import { getSharedTargeting } from './shared';
8
+ import { getViewportTargeting } from './viewport';
9
+ const filterValues = (pageTargets) => {
10
+ const filtered = {};
11
+ for (const key in pageTargets) {
12
+ const value = pageTargets[key];
13
+ if (isString(value)) {
14
+ filtered[key] = value;
15
+ }
16
+ else if (Array.isArray(value) &&
17
+ value.length > 0 &&
18
+ value.every(isString)) {
19
+ filtered[key] = value;
20
+ }
21
+ }
22
+ return filtered;
23
+ };
24
+ const lastPerformanceEntryIsNavigationType = () => {
25
+ if (!supportsPerformanceAPI()) {
26
+ return false;
27
+ }
28
+ const navigationEvents = performance.getEntriesByType('navigation');
29
+ const lastNavigationEvent = navigationEvents[navigationEvents.length - 1];
30
+ // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType#navigation
31
+ return lastNavigationEvent?.entryType === 'navigation';
32
+ };
33
+ const referrerMatchesHost = (referrer) => {
34
+ if (!referrer) {
35
+ return false;
36
+ }
37
+ const referrerUrl = new URL(referrer);
38
+ return referrerUrl.hostname === window.location.hostname;
39
+ };
40
+ // A consentless friendly way of determining if this is the users first visit to the page
41
+ const isFirstVisit = (referrer) => {
42
+ if (supportsPerformanceAPI() && !lastPerformanceEntryIsNavigationType()) {
43
+ return false;
44
+ }
45
+ return !referrerMatchesHost(referrer);
46
+ };
47
+ const buildPageTargeting = ({ adFree, clientSideParticipations, consentState, isSignedIn = false, youtube = false, }) => {
48
+ const { page, isDotcomRendering } = window.guardian.config;
49
+ const adFreeTargeting = adFree ? { af: 't' } : {};
50
+ const sharedAdTargeting = page.sharedAdTargeting
51
+ ? getSharedTargeting(page.sharedAdTargeting)
52
+ : {};
53
+ const contentTargeting = getContentTargeting({
54
+ webPublicationDate: page.webPublicationDate,
55
+ eligibleForDCR: page.dcrCouldRender,
56
+ path: `/${page.pageId}`,
57
+ renderingPlatform: isDotcomRendering
58
+ ? 'dotcom-rendering'
59
+ : 'dotcom-platform',
60
+ section: page.section,
61
+ sensitive: page.isSensitive,
62
+ videoLength: page.videoDuration,
63
+ keywords: sharedAdTargeting.k ?? [],
64
+ });
65
+ const referrer = document.referrer || '';
66
+ const sessionTargeting = getSessionTargeting({
67
+ adTest: getCookie({ name: 'adtest', shouldMemoize: true }),
68
+ countryCode: getLocale(),
69
+ isSignedIn,
70
+ pageViewId: window.guardian.config.ophan.pageViewId,
71
+ participations: {
72
+ clientSideParticipations,
73
+ serverSideParticipations: window.guardian.config.tests ?? {},
74
+ },
75
+ referrer,
76
+ });
77
+ const getViewport = () => {
78
+ return {
79
+ width: window.innerWidth || document.body.clientWidth || 0,
80
+ height: window.innerHeight || document.body.clientHeight || 0,
81
+ };
82
+ };
83
+ const viewportTargeting = getViewportTargeting({
84
+ viewPortWidth: getViewport().width,
85
+ cmpBannerWillShow: !cmp.hasInitialised() || cmp.willShowPrivacyMessageSync(),
86
+ });
87
+ const personalisedTargeting = getPersonalisedTargeting({
88
+ state: consentState,
89
+ youtube,
90
+ });
91
+ const consentlessTargeting = {};
92
+ if (!getConsentFor('googletag', consentState)) {
93
+ consentlessTargeting.firstvisit = isFirstVisit(referrer) ? 't' : 'f';
94
+ }
95
+ const pageTargets = {
96
+ ...personalisedTargeting,
97
+ ...sharedAdTargeting,
98
+ ...adFreeTargeting,
99
+ ...contentTargeting,
100
+ ...sessionTargeting,
101
+ ...viewportTargeting,
102
+ ...consentlessTargeting,
103
+ };
104
+ // filter !(string | string[]) and empty values
105
+ const pageTargeting = filterValues(pageTargets);
106
+ return pageTargeting;
107
+ };
108
+ export { buildPageTargeting, filterValues };
@@ -0,0 +1,87 @@
1
+ import type { SharedTargeting } from './shared';
2
+ import type { False, True } from './types';
3
+ declare const videoLengths: readonly ["25", "30", "60", "90", "120", "150", "180", "210", "240", "270", "300"];
4
+ /**
5
+ * Content Targeting comes from the server
6
+ *
7
+ * For a specific URL, it will only change on
8
+ * - a Composer/CAPI update
9
+ * - a rendering platform capability update
10
+ * - a main media update
11
+ * - a series tag update
12
+ * - a surge in page views per minute
13
+ *
14
+ */
15
+ type ContentTargeting = {
16
+ /**
17
+ * **D**ot**c**om-**r**endering **E**ligible - [see on Ad Manager][gam]
18
+ *
19
+ * Type: _Predefined_
20
+ *
21
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=11958028
22
+ */
23
+ dcre: True | False;
24
+ /**
25
+ * **R**ecently Published **C**ontent - [see on Ad Manager][gam]
26
+ *
27
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=13845194
28
+ */
29
+ rc: string;
30
+ /**
31
+ * Rendering Platform - [see on Ad Manager][gam]
32
+ *
33
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=11881005
34
+ */
35
+ rp: 'dotcom-rendering' | 'dotcom-platform';
36
+ /**
37
+ * Site **S**ection - [see on Ad Manager][gam]
38
+ *
39
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=173967
40
+ */
41
+ s: string;
42
+ /**
43
+ * **Sens**itive - [see on Ad Manager][gam]
44
+ *
45
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=11654206
46
+ */
47
+ sens: True | False;
48
+ /**
49
+ * URL Keywords - [see on Ad Manager][gam]
50
+ *
51
+ * Type: _Dynamic_
52
+ *
53
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=12058265
54
+ */
55
+ urlkw: string[];
56
+ /**
57
+ * **V**ideo **L**ength - [see on Ad Manager][gam]
58
+ *
59
+ * Video.JS only (?)
60
+ *
61
+ * Type: _Predefined_
62
+ *
63
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=195087
64
+ */
65
+ vl: null | (typeof videoLengths)[number];
66
+ /**
67
+ * **All** **K**ey**w**ords - [see on Ad Manager][gam]
68
+ * This is a list of all keywords on the page, including the section and the URL keywords
69
+ * Type: _Dynamic_
70
+ *
71
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=13995840
72
+ */
73
+ allkw: string[];
74
+ };
75
+ type Content = {
76
+ eligibleForDCR: boolean;
77
+ path: SharedTargeting['url'];
78
+ renderingPlatform: ContentTargeting['rp'];
79
+ section: ContentTargeting['s'];
80
+ sensitive: boolean;
81
+ videoLength?: number;
82
+ webPublicationDate: number;
83
+ keywords: string[];
84
+ };
85
+ declare const getContentTargeting: ({ eligibleForDCR, path, renderingPlatform, section, sensitive, videoLength, webPublicationDate, keywords, }: Content) => ContentTargeting;
86
+ export { getContentTargeting };
87
+ export type { ContentTargeting };
@@ -0,0 +1,73 @@
1
+ import { isString } from '@guardian/libs';
2
+ /* -- Types -- */
3
+ const videoLengths = [
4
+ '25', // TODO: confirm this is a real value
5
+ '30',
6
+ '60',
7
+ '90',
8
+ '120',
9
+ '150',
10
+ '180',
11
+ '210',
12
+ '240',
13
+ '270',
14
+ '300',
15
+ ];
16
+ /* -- Methods -- */
17
+ const getVideoLength = (videoLength) => {
18
+ const index = Math.min(Math.ceil(videoLength / 30), 10);
19
+ return videoLengths[index] ?? null;
20
+ };
21
+ const getUrlKeywords = (url) => {
22
+ const lastSegment = url
23
+ .split('/')
24
+ .filter(Boolean) // This handles a trailing slash
25
+ .slice(-1)[0];
26
+ return isString(lastSegment) ? lastSegment.split('-').filter(Boolean) : [];
27
+ };
28
+ const concatUnique = (a, b) => [
29
+ ...new Set([...a, ...b]),
30
+ ];
31
+ // "0" means content < 2 hours old
32
+ // "1" means content between 2 hours and 24 hours old.
33
+ // "2" means content between 24 hours and 3 days old
34
+ // "3" means content between 3 and 7 days old
35
+ // "4" means content between 7 days and 1 month old
36
+ // "5" means content between 1 and 10 months old
37
+ // "6" means content between 10 and 14 months old
38
+ // "7" means content more than 14 months old
39
+ const calculateRecentlyPublishedBucket = (webPublicationDate) => {
40
+ const now = Date.now();
41
+ const hoursSincePublication = (now - webPublicationDate) / 1000 / 60 / 60;
42
+ const daysSincePublication = hoursSincePublication / 24;
43
+ const monthsSincePublication = daysSincePublication / 30; // near enough for our purposes
44
+ if (hoursSincePublication < 2)
45
+ return '0';
46
+ if (hoursSincePublication < 24)
47
+ return '1';
48
+ if (daysSincePublication < 3)
49
+ return '2';
50
+ if (daysSincePublication < 7)
51
+ return '3';
52
+ if (daysSincePublication < 30)
53
+ return '4';
54
+ if (monthsSincePublication < 10)
55
+ return '5';
56
+ if (monthsSincePublication < 14)
57
+ return '6';
58
+ return '7';
59
+ };
60
+ const getContentTargeting = ({ eligibleForDCR, path, renderingPlatform, section, sensitive, videoLength, webPublicationDate, keywords, }) => {
61
+ const urlkw = getUrlKeywords(path);
62
+ return {
63
+ dcre: eligibleForDCR ? 't' : 'f',
64
+ rc: calculateRecentlyPublishedBucket(webPublicationDate),
65
+ rp: renderingPlatform,
66
+ s: section,
67
+ sens: sensitive ? 't' : 'f',
68
+ urlkw,
69
+ vl: videoLength ? getVideoLength(videoLength) : null,
70
+ allkw: concatUnique(urlkw, keywords),
71
+ };
72
+ };
73
+ export { getContentTargeting };
@@ -0,0 +1,83 @@
1
+ import type { ConsentState, TCEventStatusCode } from '@guardian/libs';
2
+ import type { False, NotApplicable, True } from './types';
3
+ declare const frequency: readonly ["0", "1", "2", "3", "4", "5", "6-9", "10-15", "16-19", "20-29", "30plus"];
4
+ type Frequency = (typeof frequency)[number];
5
+ declare const adManagerGroups: readonly ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"];
6
+ type AdManagerGroup = (typeof adManagerGroups)[number];
7
+ /**
8
+ * Personalised Targeting requires user consent
9
+ *
10
+ * It allows or prevents personalised advertising, restrict data processing
11
+ * and handles access to cookies and local storage
12
+ */
13
+ type PersonalisedTargeting = {
14
+ /**
15
+ * **A**d **M**anager **T**argeting **Gr**ou**p** – [see on Ad Manager][gam]
16
+ *
17
+ * Type: _Predefined_
18
+ *
19
+ * Sample values:
20
+ * -
21
+ *
22
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=12318099
23
+ * */
24
+ amtgrp: AdManagerGroup | null;
25
+ /**
26
+ * Interaction with TCFv2 banner – [see on Ad Manager][gam]
27
+ *
28
+ * Type: _Predefined_
29
+ *
30
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=12083384
31
+ */
32
+ cmp_interaction?: TCEventStatusCode | NotApplicable;
33
+ /**
34
+ * **TCFv2 Consent** to [all purposes] – [see on Ad Manager][gam]
35
+ *
36
+ * Type: _Predefined_
37
+ *
38
+ * [all purposes]: https://vendor-list.consensu.org/v2/vendor-list.json
39
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=12080297
40
+ * */
41
+ consent_tcfv2: True | False | NotApplicable;
42
+ /**
43
+ * **Fr**equency – [see on Ad Manager][gam]
44
+ *
45
+ * Type: _Predefined_
46
+ *
47
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=214647
48
+ */
49
+ fr: Frequency;
50
+ /**
51
+ * **P**ersonalised **A**ds Consent – [see on Ad Manager][gam]
52
+ *
53
+ * Type: _Predefined_
54
+ *
55
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=11701767
56
+ */
57
+ pa: True | False;
58
+ /**
59
+ * **Permutive** user segments – [see on Ad Manager][gam]
60
+ *
61
+ * Type: _Predefined_
62
+ *
63
+ * Values: 900+ number IDs
64
+ *
65
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=11958727
66
+ */
67
+ permutive: string[];
68
+ /**
69
+ * **R**estrict **D**ata **P**rocessing Flag – [see on Ad Manager][gam]
70
+ *
71
+ * Type: _Predefined_
72
+ *
73
+ * [gam]: https://admanager.google.com/59666047#inventory/custom_targeting/detail/custom_key_id=11701767
74
+ */
75
+ rdp: True | False | NotApplicable;
76
+ };
77
+ type Personalised = {
78
+ state: ConsentState;
79
+ youtube: boolean;
80
+ };
81
+ declare const getPersonalisedTargeting: ({ state, youtube, }: Personalised) => PersonalisedTargeting;
82
+ export { getPersonalisedTargeting };
83
+ export type { PersonalisedTargeting, AdManagerGroup, Frequency };