Skip to content

Commit 6a610ba

Browse files
authored
✨ Google Ad Manager Publisher first-party IDs (#40249)
* Add 'canSetCookie'. * Add 'tryAddingCookieParams' to amp-a4a. * Add cookie setting and clearing logic to doubleclick impl. * Fix getTempCookieName counter. * Fix doubleclick unit tests. * Add cookie clearing unit test. * Fix unit tests and convert expiration date to ms as expected. * Refactor cookie logic into util, and extend support to adsense. * Add unit tests for cookie-util.js. * Allow cokie-utils.js to depend on src/cookies.js. * Fix type. * Disallow cookie setting on proxy origin. * Make test win.location slightly more realistic. * Instead of carving out proxy origin completely, just don't set the domain. * Apply proxy origin domain filtering for gfp opt out cookie.
1 parent 398749e commit 6a610ba

File tree

11 files changed

+752
-3
lines changed

11 files changed

+752
-3
lines changed

ads/google/a4a/cookie-utils.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {setCookie} from 'src/cookies';
2+
import {isProxyOrigin} from 'src/url';
3+
4+
/** @type {string} */
5+
export const AMP_GFP_SET_COOKIES_HEADER_NAME = 'amp-ff-set-cookies';
6+
7+
/**
8+
* Returns the given domain if the current origin is not the AMP proxy origin,
9+
* otherwise returns the empty string.
10+
*
11+
* On proxy origin, we want cookies to be partitioned by subdomain to prevent
12+
* sharing across unrelated publishers, in which case we want to set the domain
13+
* equal to the empty string (leave it unset).
14+
*
15+
* @param {!Window} win
16+
* @param {string} domain
17+
* @return {string}
18+
*/
19+
function getProxySafeDomain(win, domain) {
20+
return isProxyOrigin(win.location) ? '' : domain;
21+
}
22+
23+
/**
24+
* @param {!Window} win
25+
* @param {!Response} fetchResponse
26+
*/
27+
export function maybeSetCookieFromAdResponse(win, fetchResponse) {
28+
if (!fetchResponse.headers.has(AMP_GFP_SET_COOKIES_HEADER_NAME)) {
29+
return;
30+
}
31+
let cookiesToSet = /** @type {!Array<!Object>} */ [];
32+
try {
33+
cookiesToSet = JSON.parse(
34+
fetchResponse.headers.get(AMP_GFP_SET_COOKIES_HEADER_NAME)
35+
);
36+
} catch {}
37+
for (const cookieInfo of cookiesToSet) {
38+
const cookieName =
39+
(cookieInfo['_version_'] ?? 1) === 2 ? '__gpi' : '__gads';
40+
const value = cookieInfo['_value_'];
41+
// On proxy origin, we want cookies to be partitioned by subdomain to
42+
// prevent sharing across unrelated publishers, so we don't set a domain.
43+
const domain = getProxySafeDomain(win, cookieInfo['_domain_']);
44+
const expiration = Math.max(cookieInfo['_expiration_'], 0);
45+
setCookie(win, cookieName, value, expiration, {
46+
domain,
47+
secure: false,
48+
});
49+
}
50+
}
51+
52+
/**
53+
* Sets up postmessage listener for cookie opt out signal.
54+
* @param {!Window} win
55+
* @param {!Event} event
56+
*/
57+
export function handleCookieOptOutPostMessage(win, event) {
58+
try {
59+
const message = JSON.parse(event.data);
60+
if (message['googMsgType'] === 'gpi-uoo') {
61+
const userOptOut = !!message['userOptOut'];
62+
const clearAdsData = !!message['clearAdsData'];
63+
const domain = getProxySafeDomain(win, win.location.hostname);
64+
setCookie(
65+
win,
66+
'__gpi_opt_out',
67+
userOptOut ? '1' : '0',
68+
// Last valid date for 32-bit browsers; 2038-01-19
69+
2147483646 * 1000,
70+
{domain}
71+
);
72+
if (userOptOut || clearAdsData) {
73+
setCookie(win, '__gads', 'delete', Date.now() - 1000, {domain});
74+
setCookie(win, '__gpi', 'delete', Date.now() - 1000, {domain});
75+
}
76+
}
77+
} catch {}
78+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import {
2+
AMP_GFP_SET_COOKIES_HEADER_NAME,
3+
handleCookieOptOutPostMessage,
4+
maybeSetCookieFromAdResponse,
5+
} from '#ads/google/a4a/cookie-utils';
6+
7+
import {getCookie, setCookie} from 'src/cookies';
8+
9+
describes.fakeWin('#maybeSetCookieFromAdResponse', {amp: true}, (env) => {
10+
it('should set cookies based on ad response header', () => {
11+
maybeSetCookieFromAdResponse(env.win, {
12+
headers: {
13+
has: (header) => {
14+
return header === AMP_GFP_SET_COOKIES_HEADER_NAME;
15+
},
16+
get: (header) => {
17+
if (header !== AMP_GFP_SET_COOKIES_HEADER_NAME) {
18+
return;
19+
}
20+
21+
return JSON.stringify([
22+
{
23+
'_version_': 1,
24+
'_value_': 'val1',
25+
'_domain_': 'foo.com',
26+
'_expiration_': Date.now() + 100_000,
27+
},
28+
{
29+
'_version_': 2,
30+
'_value_': 'val2',
31+
'_domain_': 'foo.com',
32+
'_expiration_': Date.now() + 100_000,
33+
},
34+
]);
35+
},
36+
},
37+
});
38+
39+
expect(getCookie(env.win, '__gads')).to.equal('val1');
40+
expect(getCookie(env.win, '__gpi')).to.equal('val2');
41+
});
42+
});
43+
44+
describes.fakeWin('#handleCookieOptOutPostMessage', {amp: true}, (env) => {
45+
it('should clear cookies as specified in creative response, with opt out', () => {
46+
setCookie(env.win, '__gads', '__gads_val', Date.now() + 100_000);
47+
setCookie(env.win, '__gpi', '__gpi_val', Date.now() + 100_000);
48+
expect(getCookie(env.win, '__gads')).to.equal('__gads_val');
49+
expect(getCookie(env.win, '__gpi')).to.equal('__gpi_val');
50+
51+
handleCookieOptOutPostMessage(env.win, {
52+
data: JSON.stringify({
53+
googMsgType: 'gpi-uoo',
54+
userOptOut: true,
55+
clearAdsData: true,
56+
}),
57+
});
58+
59+
expect(getCookie(env.win, '__gpi_opt_out')).to.equal('1');
60+
expect(getCookie(env.win, '__gads')).to.be.null;
61+
expect(getCookie(env.win, '__gpi')).to.be.null;
62+
});
63+
64+
it('should clear cookies as specified in creative response, without opt out', () => {
65+
setCookie(env.win, '__gads', '__gads_val', Date.now() + 100_000);
66+
setCookie(env.win, '__gpi', '__gpi_val', Date.now() + 100_000);
67+
expect(getCookie(env.win, '__gads')).to.equal('__gads_val');
68+
expect(getCookie(env.win, '__gpi')).to.equal('__gpi_val');
69+
70+
handleCookieOptOutPostMessage(env.win, {
71+
data: JSON.stringify({
72+
googMsgType: 'gpi-uoo',
73+
userOptOut: false,
74+
clearAdsData: true,
75+
}),
76+
});
77+
78+
expect(getCookie(env.win, '__gpi_opt_out')).to.equal('0');
79+
expect(getCookie(env.win, '__gads')).to.be.null;
80+
expect(getCookie(env.win, '__gpi')).to.be.null;
81+
});
82+
83+
it('should not clear cookies as specified in creative response, without opt out or clear ads', () => {
84+
setCookie(env.win, '__gads', '__gads_val', Date.now() + 100_000);
85+
setCookie(env.win, '__gpi', '__gpi_val', Date.now() + 100_000);
86+
expect(getCookie(env.win, '__gads')).to.equal('__gads_val');
87+
expect(getCookie(env.win, '__gpi')).to.equal('__gpi_val');
88+
89+
handleCookieOptOutPostMessage(env.win, {
90+
data: JSON.stringify({
91+
googMsgType: 'gpi-uoo',
92+
userOptOut: false,
93+
clearAdsData: false,
94+
}),
95+
});
96+
97+
expect(getCookie(env.win, '__gpi_opt_out')).to.equal('0');
98+
expect(getCookie(env.win, '__gads')).to.equal('__gads_val');
99+
expect(getCookie(env.win, '__gpi')).to.equal('__gpi_val');
100+
});
101+
});

build-system/test-configs/dep-check-config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ exports.rules = [
108108
'ads/google/a4a/**->src/ad-cid.js',
109109
'ads/google/a4a/**->src/experiments/index.js',
110110
'ads/google/a4a/**->src/service/index.js',
111+
'ads/google/a4a/cookie-utils.js->src/cookies.js',
111112
'ads/google/a4a/utils.js->src/service/variable-source.js',
112113
'ads/google/a4a/utils.js->src/ini-load.js',
113114
// IMA, similar to other non-Ad 3Ps above, needs access to event-helper

extensions/amp-a4a/0.1/amp-a4a.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {listenOnce} from '#utils/event-helper';
3030
import {dev, devAssert, logHashParam, user, userAssert} from '#utils/log';
3131
import {isAttributionReportingAllowed} from '#utils/privacy-sandbox-utils';
3232

33+
import {canSetCookie, getCookie} from 'src/cookies';
34+
3335
import {A4AVariableSource} from './a4a-variable-source';
3436
import {getExtensionsFromMetadata} from './amp-ad-utils';
3537
import {processHead} from './head-validation';
@@ -840,6 +842,15 @@ export class AmpA4A extends AMP.BaseElement {
840842
// response is empty.
841843
/** @return {!Promise<!Response>} */
842844
.then((fetchResponse) => {
845+
protectFunctionWrapper(this.onAdResponse, this, (err) => {
846+
dev().error(
847+
TAG,
848+
this.element.getAttribute('type'),
849+
'Error executing onAdResponse',
850+
err
851+
);
852+
})(fetchResponse);
853+
843854
checkStillCurrent();
844855
this.maybeTriggerAnalyticsEvent_('adRequestEnd');
845856
// If the response is null (can occur for non-200 responses) or
@@ -1672,6 +1683,12 @@ export class AmpA4A extends AMP.BaseElement {
16721683
);
16731684
}
16741685

1686+
/**
1687+
* To be overridden by implementing subclasses.
1688+
* @param {!Response} unusedFetchResponse
1689+
*/
1690+
onAdResponse(unusedFetchResponse) {}
1691+
16751692
/**
16761693
* @param {!Element} iframe that was just created. To be overridden for
16771694
* testing.
@@ -2592,3 +2609,46 @@ export function isPlatformSupported(win) {
25922609
function isNative(func) {
25932610
return !!func && func.toString().indexOf('[native code]') != -1;
25942611
}
2612+
2613+
/**
2614+
* @param {?ConsentTupleDef} consentTuple
2615+
* @return {boolean}
2616+
*/
2617+
export function hasStorageConsent(consentTuple) {
2618+
if (!consentTuple) {
2619+
return false;
2620+
}
2621+
2622+
if (
2623+
[CONSENT_POLICY_STATE.UNKNOWN, CONSENT_POLICY_STATE.INSUFFICIENT].includes(
2624+
consentTuple.consentState
2625+
)
2626+
) {
2627+
return false;
2628+
}
2629+
2630+
const {consentString, gdprApplies, purposeOne} = consentTuple;
2631+
2632+
if (!gdprApplies) {
2633+
return true;
2634+
}
2635+
2636+
return consentString && purposeOne;
2637+
}
2638+
2639+
/**
2640+
* @param {?ConsentTupleDef} consentTuple
2641+
* @param {!Window} win
2642+
* @param {!{[key: string]: string|boolean|number}} params
2643+
*/
2644+
export function tryAddingCookieParams(consentTuple, win, params) {
2645+
if (!hasStorageConsent(consentTuple)) {
2646+
return;
2647+
}
2648+
const cookie = getCookie(win, '__gads');
2649+
params['cookie'] = cookie;
2650+
params['gpic'] = getCookie(win, '__gpi');
2651+
if (!cookie && canSetCookie(win)) {
2652+
params['cookie_enabled'] = '1';
2653+
}
2654+
}

0 commit comments

Comments
 (0)