Skip to content

Commit

Permalink
Implement ad size optimization for AdSense.
Browse files Browse the repository at this point in the history
See #23568.
  • Loading branch information
riklund committed Oct 1, 2019
1 parent b07639e commit bd0ff12
Show file tree
Hide file tree
Showing 6 changed files with 633 additions and 28 deletions.
4 changes: 3 additions & 1 deletion build-system/tasks/presubmit-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -465,15 +465,17 @@ const forbiddenTerms = {
// https://amp.dev/documentation/guides-and-tutorials/learn/a4a_spec
'src/services.js',
'src/service/cid-impl.js',
'extensions/amp-user-notification/0.1/amp-user-notification.js',
'extensions/amp-ad-network-adsense-impl/0.1/responsive-state.js',
'extensions/amp-app-banner/0.1/amp-app-banner.js',
'extensions/amp-consent/0.1/consent-state-manager.js',
'extensions/amp-user-notification/0.1/amp-user-notification.js',
],
},
'localStorage': {
message: requiresReviewPrivacy,
whitelist: [
'extensions/amp-access/0.1/amp-access-iframe.js',
'extensions/amp-ad-network-adsense-impl/0.1/amp-ad-network-adsense-impl.js',
'extensions/amp-script/0.1/amp-script.js',
'extensions/amp-web-push/0.1/amp-web-push-helper-frame.js',
'extensions/amp-web-push/0.1/amp-web-push-permission-dialog.js',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@ export class AmpAdNetworkAdsenseImpl extends AmpA4A {
return getAmpAdRenderOutsideViewport(this.element) || 3;
}

/** @override */
/** @override
@return {!Promise|undefined}.
*/
buildCallback() {
super.buildCallback();
this.identityTokenPromise_ = this.getAmpDoc()
Expand All @@ -181,13 +183,21 @@ export class AmpAdNetworkAdsenseImpl extends AmpA4A {
getIdentityToken(this.win, this.getAmpDoc(), super.getConsentPolicy())
);

if (this.responsiveState_ != null) {
return this.responsiveState_.attemptChangeSize();
}
// This should happen last, as some diversion criteria rely on some of the
// preceding logic (specifically responsive logic).
this.divertExperiments();
this.maybeAddSinglePassExperiment();
return ResponsiveState.maybeUpgradeToResponsive(
this.element,
this.getAdClientId_()
).then(state => {
if (state != null) {
this.responsiveState_ = state;
}
if (this.responsiveState_ != null) {
return this.responsiveState_.attemptChangeSize();
}
// This should happen last, as some diversion criteria rely on some of the
// preceding logic (specifically responsive logic).
this.divertExperiments();
this.maybeAddSinglePassExperiment();
});
}

/** @override */
Expand Down Expand Up @@ -225,6 +235,20 @@ export class AmpAdNetworkAdsenseImpl extends AmpA4A {
);
}

/**
* @return {string} ad client ID for the current ad unit.
* @private
*/
getAdClientId_() {
const adClientId = (
this.element.getAttribute('data-ad-client') || ''
).toLowerCase();
if (!/^ca-/i.test(adClientId)) {
return `ca-${adClientId}`;
}
return adClientId;
}

/** @override */
getAdUrl(consentState) {
if (
Expand All @@ -238,12 +262,7 @@ export class AmpAdNetworkAdsenseImpl extends AmpA4A {
// validateData, from 3p/3p/js, after moving it someplace common.
const startTime = Date.now();
const global = this.win;
let adClientId = this.element.getAttribute('data-ad-client');
// Ensure client id format: lower case with 'ca-' prefix.
adClientId = adClientId.toLowerCase();
if (adClientId.substring(0, 3) != 'ca-') {
adClientId = 'ca-' + adClientId;
}
const adClientId = this.getAdClientId_();
const adTestOn =
this.element.getAttribute('data-adtest') ||
isInManualExperiment(this.element);
Expand Down Expand Up @@ -432,6 +451,13 @@ export class AmpAdNetworkAdsenseImpl extends AmpA4A {
/** @override */
onCreativeRender(creativeMetaData) {
super.onCreativeRender(creativeMetaData);
if (this.iframe != null) {
ResponsiveState.maybeAttachSettingsListener(
this.element,
this.iframe,
this.getAdClientId_()
);
}
this.isAmpCreative_ = !!creativeMetaData;
if (
creativeMetaData &&
Expand Down
176 changes: 173 additions & 3 deletions extensions/amp-ad-network-adsense-impl/0.1/responsive-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {Services} from '../../../src/services';
import {addExperimentIdToElement} from '../../../ads/google/a4a/traffic-experiments';
import {clamp} from '../../../src/utils/math';
import {computedStyle, setStyle} from '../../../src/style';
import {dev, user} from '../../../src/log';
import {dev, devAssert, user} from '../../../src/log';
import {getData} from '../../../src/event-helper';
import {hasOwn} from '../../../src/utils/object';
import {randomlySelectUnsetExperiments} from '../../../src/experiments';
import {toWin} from '../../../src/types';
Expand All @@ -40,6 +41,15 @@ const RAFMT_PARAMS = {
[ADSENSE_MCRSPV_TAG]: 15,
};

/** @const {!{branch: string, control: string, experiment: string}}
@visibleForTesting
*/
export const AD_SIZE_OPTIMIZATION_EXP = {
branch: 'adsense-ad-size-optimization',
control: '368226510',
experiment: '368226511',
};

/** @const {!{branch: string, control: string, experiment: string}}
@visibleForTesting
*/
Expand Down Expand Up @@ -78,6 +88,141 @@ export class ResponsiveState {
return new ResponsiveState(element);
}

/**
* Upgrades the ad unit to responsive if there is an opt-in setting in localstorage.
* See https://github.com/ampproject/amphtml/issues/23568 for design.
* @param {!Element} element
* @param {string} adClientId
* @return {!Promise<?ResponsiveState>} a promise that resolves when any upgrade is complete.
*/
static maybeUpgradeToResponsive(element, adClientId) {
if (!ResponsiveState.isInAdSizeOptimizationExperimentBranch_(element)) {
return Promise.resolve(null);
}
// If the ad unit is already responsive we don't upgrade again.
if (element.hasAttribute('data-auto-format')) {
return Promise.resolve(null);
}
return (
Services.storageForDoc(element)
.then(storage =>
storage.get(
ResponsiveState.getAdSizeOptimizationStorageKey_(adClientId)
)
)
.then(isAdSizeOptimizationEnabled => {
if (isAdSizeOptimizationEnabled) {
return ResponsiveState.upgradeToResponsive_(element);
}
return null;
})
// Do nothing if we fail to read localstorage.
.catch(() => {
dev().warn(TAG, 'Failed to look up publisher ad size settings.');
return null;
})
);
}

/**
* Upgrades the element to responsive.
*
* @param {!Element} element
* @return {!ResponsiveState} responsive state
* @private
*/
static upgradeToResponsive_(element) {
element.setAttribute('height', ADSENSE_RSPV_WHITELISTED_HEIGHT);
element.setAttribute('width', '100vw');
element.setAttribute('data-full-width', '');
element.setAttribute('data-auto-format', 'rspv');

const state = ResponsiveState.createIfResponsive(element);
devAssert(state != null, 'Upgrade failed');
return /** @type {!ResponsiveState} */ (state);
}

/**
* Sets up a listener for a pingback of publisher settings in the ad response and
* writing such settings to localstorage.
*
* Note that we can have multiple listeners on the same page, which is okay because
* the settings for publishers should not change between different slots.
*
* Once the listener has received one valid setting update event, it will remove
* itself.
*
* See https://github.com/ampproject/amphtml/issues/23568 for design.
*
* @param {!Element} element to use for fetching storage.
* @param {!HTMLIFrameElement} iframe ad iframe.
* @param {string} adClientId
* @return {?Promise} a promise that resolves when ad size settings are updated, or null if no listener was attached.
*/
static maybeAttachSettingsListener(element, iframe, adClientId) {
if (!ResponsiveState.isInAdSizeOptimizationExperimentBranch_(element)) {
return null;
}
let promiseResolver;
const savePromise = new Promise(resolve => {
promiseResolver = resolve;
});
const win = toWin(element.ownerDocument.defaultView);

const listener = event => {
if (event['source'] != iframe.contentWindow) {
return;
}
const data = getData(event);
// data will look like this:
// {
// 'googMsgType': 'adsense-settings',
// 'adClient': 'ca-pub-123',
// 'enableAutoAdSize': '1'
// }
if (!!data && data['googMsgType'] != 'adsense-settings') {
return;
}
if (data['adClient'] != adClientId) {
return;
}

const autoAdSizeStatus = data['enableAutoAdSize'] == '1';
win.removeEventListener('message', listener);

Services.storageForDoc(element)
.then(storage =>
storage
.set(
ResponsiveState.getAdSizeOptimizationStorageKey_(adClientId),
autoAdSizeStatus
)
.then(() => {
dev().info(
TAG,
`Saved publisher auto ad size setting: ${autoAdSizeStatus}`
);
promiseResolver();
})
)
// Do nothing if we fail to write to localstorage.
.catch(() => {
dev().warn(TAG, 'Failed to persist publisher auto ad size setting.');
});
};
win.addEventListener('message', listener);
return savePromise;
}

/**
* @param {string} adClientId
* @return {string} ad size optimization storage key.
* @private
*/
static getAdSizeOptimizationStorageKey_(adClientId) {
return `aas-${adClientId}`;
}

/** @return {boolean} */
isValidElement() {
if (!this.element_.hasAttribute('data-full-width')) {
Expand Down Expand Up @@ -158,10 +303,35 @@ export class ResponsiveState {
return RAFMT_PARAMS[this.getAutoFormat_()];
}

/**
* Selects into the ad size optimization experiment.
* @param {!Element} element
* @return {boolean}
*/
static isInAdSizeOptimizationExperimentBranch_(element) {
const experimentInfoMap = /** @type {!Object<string,
!../../../src/experiments.ExperimentInfo>} */ ({
[[AD_SIZE_OPTIMIZATION_EXP.branch]]: {
isTrafficEligible: () => true,
branches: [
[AD_SIZE_OPTIMIZATION_EXP.control],
[AD_SIZE_OPTIMIZATION_EXP.experiment],
],
},
});
const win = toWin(element.ownerDocument.defaultView);
const setExps = randomlySelectUnsetExperiments(win, experimentInfoMap);
Object.keys(setExps).forEach(expName =>
addExperimentIdToElement(setExps[expName], element)
);
return (
setExps[AD_SIZE_OPTIMIZATION_EXP.branch] ==
AD_SIZE_OPTIMIZATION_EXP.experiment
);
}

/**
* Selects into the inconsistent responsive height fix experiment.
* Note that this needs to be done before responsive sizing, so it must
* be separate from divertExperiments below.
* @return {boolean}
* @private
*/
Expand Down

0 comments on commit bd0ff12

Please sign in to comment.