diff --git a/ads/_a4a-config.js b/ads/_a4a-config.js index 7eaa40fb5323..aeddc19f4653 100644 --- a/ads/_a4a-config.js +++ b/ads/_a4a-config.js @@ -27,6 +27,7 @@ export function getA4ARegistry() { 'dianomi': () => true, 'doubleclick': () => true, 'fake': () => true, + 'mgid': (win, adTag) => !adTag.hasAttribute('data-container'), 'nws': () => true, 'smartadserver': () => true, 'valueimpression': () => true, diff --git a/build-system/compile/bundles.config.extensions.json b/build-system/compile/bundles.config.extensions.json index 68904689a405..b7bf3beabae0 100644 --- a/build-system/compile/bundles.config.extensions.json +++ b/build-system/compile/bundles.config.extensions.json @@ -113,6 +113,11 @@ "version": "0.1", "latestVersion": "0.1" }, + { + "name": "amp-ad-network-mgid-impl", + "version": "0.1", + "latestVersion": "0.1" + }, { "name": "amp-ad-network-nws-impl", "version": "0.1", diff --git a/build-system/test-configs/dep-check-config.js b/build-system/test-configs/dep-check-config.js index d497c30665f0..94f2466c883e 100644 --- a/build-system/test-configs/dep-check-config.js +++ b/build-system/test-configs/dep-check-config.js @@ -150,6 +150,7 @@ exports.rules = [ 'extensions/amp-ad-network-valueimpression-impl/0.1/amp-ad-network-valueimpression-impl.js->extensions/amp-a4a/0.1/amp-a4a.js', 'extensions/amp-ad-network-dianomi-impl/0.1/amp-ad-network-dianomi-impl.js->extensions/amp-a4a/0.1/amp-a4a.js', 'extensions/amp-ad-network-smartadserver-impl/0.1/amp-ad-network-smartadserver-impl.js->extensions/amp-a4a/0.1/amp-a4a.js', + 'extensions/amp-ad-network-mgid-impl/0.1/amp-ad-network-mgid-impl.js->extensions/amp-a4a/0.1/amp-a4a.js', // A4A impls importing amp fast fetch header name 'extensions/amp-ad-network-adsense-impl/0.1/amp-ad-network-adsense-impl.js->extensions/amp-a4a/0.1/signature-verifier.js', diff --git a/examples/amp-story/mgid.html b/examples/amp-story/mgid.html new file mode 100644 index 000000000000..6d0054c52b16 --- /dev/null +++ b/examples/amp-story/mgid.html @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + +

1

+
+
+ + + +

2

+
+
+ + + +

3

+
+
+ + + +

4

+
+
+ + + +

5

+
+
+ + + +

6

+
+
+ + + +

7

+
+
+ + + +

8

+
+
+ + + +

9

+
+
+ + + +

10

+
+
+ + + +

11

+
+
+ + + +

12

+
+
+ + + +

13

+
+
+ + + +

14

+
+
+ + + +

15

+
+
+ + + +

16

+
+
+ + + +

17

+
+
+ + + +

18

+
+
+ + + +

19

+
+
+ + + +

20

+
+
+ +
+ + + diff --git a/extensions/amp-ad-network-mgid-impl/0.1/amp-ad-network-mgid-impl.js b/extensions/amp-ad-network-mgid-impl/0.1/amp-ad-network-mgid-impl.js new file mode 100644 index 000000000000..755620a92c39 --- /dev/null +++ b/extensions/amp-ad-network-mgid-impl/0.1/amp-ad-network-mgid-impl.js @@ -0,0 +1,293 @@ +import { + CONSENT_POLICY_STATE, + CONSENT_STRING_TYPE, +} from '#core/constants/consent-state'; +import {createElementWithAttributes, removeElement} from '#core/dom'; +import {hasOwn} from '#core/types/object'; + +import {Services} from '#service'; + +import {user} from '#utils/log'; + +import {AmpA4A} from '../../amp-a4a/0.1/amp-a4a'; + +/** @const {string} */ +const TAG = 'amp-ad-network-mgid-impl'; + +const BASE_URL_ = 'https://servicer.mgid.com/'; +const PV_URL_ = 'https://c.mgid.com/pv/'; + +export class AmpAdNetworkMgidImpl extends AmpA4A { + /** + * @param {!Element} element + */ + constructor(element) { + super(element); + + /** @private {?Element} */ + this.ampAnalyticsElement_ = null; + } + + /** @override */ + tearDownSlot() { + super.tearDownSlot(); + if (this.ampAnalyticsElement_) { + removeElement(this.ampAnalyticsElement_); + this.ampAnalyticsElement_ = null; + } + } + + /** @override */ + isValidElement() { + const id = this.element.getAttribute('data-widget'); + if (!id || parseInt(id, 10) != id) { + user().warn(TAG, 'Undefined or non-numeric data-widget param!'); + return false; + } + return true; + } + + /** @override */ + getAdUrl(consentTuple, opt_rtcResponsesPromise) { + let adUrlParams = []; + + const consentParams = this.getConsents_(consentTuple); + if (consentParams.length !== 0) { + adUrlParams = adUrlParams.concat(consentParams); + } + + const widget = this.element.getAttribute('data-widget'); + + let servicerUrl = BASE_URL_ + widget + '/' + this.getPageParam_(); + + adUrlParams = adUrlParams.concat(this.getNetworkInfoParams_()); + adUrlParams.push(this.getCacheBusterParam_()); + adUrlParams.push(this.getDevicePixelRatioParam_()); + adUrlParams.push(this.getCxurlParam_()); + adUrlParams.push(this.getPrParam_()); + adUrlParams.push(this.getPvidParam_()); + adUrlParams.push(this.getMuidParam_()); + adUrlParams.push('implVersion=15'); + + return Promise.allSettled(adUrlParams).then((params) => { + const data = []; + params.forEach((result) => data.push(result.value)); + const joinedParams = '?' + data.join('&'); + servicerUrl += joinedParams; + + if (!hasOwn(this.win, '_mgAmpStoryPV')) { + this.getAmpDoc() + .getBody() + .appendChild( + createElementWithAttributes(this.win.document, 'amp-pixel', { + 'src': PV_URL_ + joinedParams, + }) + ); + + this.win['_mgAmpStoryPV'] = 1; + } + + return servicerUrl; + }); + } + + /** + * @param {!ConsentTupleDef=} consentTuple + * @return {string[]} Consents parameters. + * @private + */ + getConsents_(consentTuple) { + const result = []; + + let consentState = undefined; + let consentString = undefined; + let gdprApplies = undefined; + let consentStringType = undefined; + if (consentTuple) { + consentState = consentTuple.consentState; + consentString = consentTuple.consentString; + gdprApplies = consentTuple.gdprApplies; + consentStringType = consentTuple.consentStringType; + } + if ( + consentState === CONSENT_POLICY_STATE.UNKNOWN && + this.element.getAttribute('data-npa-on-unknown-consent') !== 'true' + ) { + return result; + } + + if (gdprApplies) { + result.push( + 'gdprApplies=' + + (gdprApplies === true ? '1' : gdprApplies === false ? '0' : null) + ); + } + + if ( + consentString && + consentStringType != CONSENT_STRING_TYPE.US_PRIVACY_STRING + ) { + result.push('consentData=' + consentString); + } + + if ( + consentString && + consentStringType == CONSENT_STRING_TYPE.US_PRIVACY_STRING + ) { + result.push('uspString=' + consentString); + } + + return result; + } + + /** + * @return {string} Page data for ad request + * @private + */ + getPageParam_() { + const counter = Services.urlReplacementsForDoc( + this.element + )./*OK*/ expandStringSync('COUNTER', undefined, { + 'COUNTER': true, + }); + return parseInt(counter, 10) % 20; + } + + /** + * @return {string} Cachebuster for ad request + * @private + */ + getCacheBusterParam_() { + return ( + 'cbuster=' + + Date.now().toString() + + Math.floor(Math.random() * 1000000000 + 1) + ); + } + + /** + * @return {string} Network information data for ad request + * @private + */ + getNetworkInfoParams_() { + const params = []; + try { + const networkInformation = + navigator.connection || + navigator.mozConnection || + navigator.webkitConnection; + + if (typeof networkInformation.type != 'undefined') { + params.push('nit=' + networkInformation.type); + } + if (typeof networkInformation.effectiveType != 'undefined') { + params.push('niet=' + networkInformation.effectiveType); + } + if (typeof networkInformation.saveData != 'undefined') { + params.push('nisd=' + (networkInformation.saveData ? 1 : 0)); + } + } catch (e) {} + + return params; + } + + /** + * @return {string} Device pixel ratio info for ad request + * @private + */ + getDevicePixelRatioParam_() { + let ratio = 1; + + if (typeof window.devicePixelRatio !== 'undefined') { + ratio = window.devicePixelRatio; + } else if ( + typeof window.screen.systemXDPI !== 'undefined' && + typeof window.screen.logicalXDPI !== 'undefined' && + window.screen.systemXDPI > window.screen.logicalXDPI + ) { + ratio = window.screen.systemXDPI / window.screen.logicalXDPI; + } + + const isInt = ratio % 1 === 0; + + if (!isInt) { + ratio = ratio.toFixed(3); + } + + return 'dpr=' + ratio; + } + + /** + * @return {string} Referrer info for ad request + * @private + */ + getPrParam_() { + return this.getReferrer_(10).then((referrer) => { + const matchDomain = referrer.match(/:\/\/([^\/:]+)/i); + return ( + 'pr=' + + encodeURIComponent(matchDomain && matchDomain[1] ? matchDomain[1] : '') + ); + }); + } + + /** + * @return {string} Current page url for ad request + * @private + */ + getCxurlParam_() { + const url = Services.documentInfoForDoc(this.element).canonicalUrl; + return 'cxurl=' + encodeURIComponent(url); + } + + /** + * @return {string} Pageview info for ad request + * @private + */ + getPvidParam_() { + return Services.documentInfoForDoc(this.element).pageViewId64.then( + (pvid) => { + return 'pvid=' + pvid; + } + ); + } + + /** + * @return {string} Muidn info for ad request + * @private + */ + getMuidParam_() { + return Services.urlReplacementsForDoc(this.element) + ./*OK*/ expandStringAsync('CLIENT_ID(muidn)', undefined, { + 'CLIENT_ID': true, + }) + .then((r) => { + return 'muid=' + r; + }); + } + + /** + * Returns the referrer or undefined if the referrer is not resolved + * before the given timeout + * @param {number=} opt_timeout + * @return {!(Promise|Promise)} A promise with a referrer or undefined + * if timed out + * @private + */ + getReferrer_(opt_timeout) { + const timeoutInt = parseInt(opt_timeout, 10); + const referrerPromise = Services.viewerForDoc( + this.getAmpDoc() + ).getReferrerUrl(); + if (isNaN(timeoutInt) || timeoutInt < 0) { + return referrerPromise; + } + return Services.timerFor(this.win) + .timeoutPromise(timeoutInt, referrerPromise) + .catch(() => undefined); + } +} + +AMP.extension('amp-ad-network-mgid-impl', '0.1', (AMP) => { + AMP.registerElement('amp-ad-network-mgid-impl', AmpAdNetworkMgidImpl); +}); diff --git a/extensions/amp-ad-network-mgid-impl/0.1/test/test-amp-ad-network-mgid-impl.js b/extensions/amp-ad-network-mgid-impl/0.1/test/test-amp-ad-network-mgid-impl.js new file mode 100644 index 000000000000..b961017f5de2 --- /dev/null +++ b/extensions/amp-ad-network-mgid-impl/0.1/test/test-amp-ad-network-mgid-impl.js @@ -0,0 +1,69 @@ +import {Services} from '#service'; + +import {AmpAd} from '../../../amp-ad/0.1/amp-ad'; // eslint-disable-line @typescript-eslint/no-unused-vars +import {AmpAdNetworkMgidImpl} from '../amp-ad-network-mgid-impl'; + +describes.realWin( + 'amp-ad-network-mgid-impl', + { + amp: { + extensions: ['amp-ad', 'amp-ad-network-mgid-impl'], + }, + }, + (env) => { + let doc; + let win; + let mgidImplElem; + beforeEach(() => { + win = env.win; + doc = win.document; + mgidImplElem = doc.createElement('amp-ad'); + mgidImplElem.setAttribute('type', 'mgid'); + mgidImplElem.setAttribute('layout', 'fixed'); + mgidImplElem.setAttribute('width', '300'); + mgidImplElem.setAttribute('height', '250'); + + env.win.document.body.appendChild(mgidImplElem); + }); + + it('should check for data-widget attribute', () => { + const mgidImpl = new AmpAdNetworkMgidImpl(mgidImplElem); + expect(mgidImpl.isValidElement()).to.be.false; + + mgidImplElem.setAttribute('data-widget', '100'); + const mgidImpl2 = new AmpAdNetworkMgidImpl(mgidImplElem); + expect(mgidImpl2.isValidElement()).to.be.true; + }); + + it('generates correct adUrl', () => { + mgidImplElem.setAttribute('data-widget', '100'); + + const viewer = Services.viewerForDoc(mgidImplElem); + env.sandbox + .stub(viewer, 'getReferrerUrl') + .returns(Promise.resolve('http://fake.example/?foo=bar')); + + const documentInfo = Services.documentInfoForDoc(mgidImplElem); + documentInfo.canonicalUrl = 'http://canonical.example/?abc=xyz'; + + const mgidImpl = new AmpAdNetworkMgidImpl(mgidImplElem); + + return mgidImpl.getAdUrl().then((url) => { + [ + /^https:\/\/servicer\.mgid\.com\/100\/1/, + /(\?|&)niet=(slow-2g|2g|3g|4g)(&|$)/, + /(\?|&)nisd=(0|1)(&|$)/, + /(\?|&)cbuster=\d+(&|$)/, + /(\?|&)dpr=\d+(&|$)/, + /(\?|&)cxurl=http%3A%2F%2Fcanonical.example%2F%3Fabc%3Dxyz(&|$)/, + /(\?|&)pr=fake.example(&|$)/, + /(\?|&)pvid=[A-z0-9\-_]+(&|$)/, + /(\?|&)muid=amp-[A-z0-9\-_]+(&|$)/, + /(\?|&)implVersion=15(&|$)/, + ].forEach((regexp) => { + expect(url).to.match(regexp); + }); + }); + }); + } +); diff --git a/extensions/amp-ad-network-mgid-impl/OWNERS b/extensions/amp-ad-network-mgid-impl/OWNERS new file mode 100644 index 000000000000..1b8c3b93f6d9 --- /dev/null +++ b/extensions/amp-ad-network-mgid-impl/OWNERS @@ -0,0 +1,10 @@ +// For an explanation of the OWNERS rules and syntax, see: +// https://github.com/ampproject/amp-github-apps/blob/main/owners/OWNERS.example + +{ + rules: [ + { + owners: [{name: 'ampproject/wg-ads-reviewers'}], + }, + ], +} diff --git a/extensions/amp-ad-network-mgid-impl/amp-ad-network-mgid-impl-internal.md b/extensions/amp-ad-network-mgid-impl/amp-ad-network-mgid-impl-internal.md new file mode 100644 index 000000000000..85faf1aeb517 --- /dev/null +++ b/extensions/amp-ad-network-mgid-impl/amp-ad-network-mgid-impl-internal.md @@ -0,0 +1,33 @@ +# amp-ad-network-mgid-impl + +Mgid implementation of AMP Ad, which is only used for amp-stories. +3p iframe implementation is used for all other cases. + + + + + + + + + + +
AvailabilityIn Development
Required Script<script async custom-element="amp-story" src="https://cdn.ampproject.org/v0/amp-story-1.0.js"></script><script async custom-element="amp-story-auto-ads" src="https://cdn.ampproject.org/v0/amp-story-auto-ads-0.1.js"></script>
+ +## Example + +```html + + + + + ... + +``` diff --git a/extensions/amp-story-auto-ads/0.1/story-ad-config.js b/extensions/amp-story-auto-ads/0.1/story-ad-config.js index 8413d7b7fc83..b97a3c0b6142 100644 --- a/extensions/amp-story-auto-ads/0.1/story-ad-config.js +++ b/extensions/amp-story-auto-ads/0.1/story-ad-config.js @@ -23,6 +23,7 @@ const AllowedAdTypes = { 'doubleclick': true, 'fake': true, 'nws': true, + 'mgid': true, }; export class StoryAdConfig {