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.
+
+
+
+ | Availability |
+ In 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 {