diff --git a/3p/integration.js b/3p/integration.js
index c8a07dea8761..7ff81d0f4eea 100644
--- a/3p/integration.js
+++ b/3p/integration.js
@@ -237,7 +237,6 @@ import {sharethrough} from '#ads/vendors/sharethrough';
import {shemedia} from '#ads/vendors/shemedia';
import {sklik} from '#ads/vendors/sklik';
import {slimcutmedia} from '#ads/vendors/slimcutmedia';
-import {smartadserver} from '#ads/vendors/smartadserver';
import {smartclip} from '#ads/vendors/smartclip';
import {smi2} from '#ads/vendors/smi2';
import {smilewanted} from '#ads/vendors/smilewanted';
@@ -528,7 +527,6 @@ register('shemedia', shemedia);
register('sklik', sklik);
register('ssp', ssp);
register('slimcutmedia', slimcutmedia);
-register('smartadserver', smartadserver);
register('smartclip', smartclip);
register('smi2', smi2);
register('smilewanted', smilewanted);
diff --git a/3p/vendors/smartadserver.js b/3p/vendors/smartadserver.js
deleted file mode 100644
index b93432464cc0..000000000000
--- a/3p/vendors/smartadserver.js
+++ /dev/null
@@ -1,12 +0,0 @@
-// src/polyfills.js must be the first import.
-import '#3p/polyfills';
-
-import {register} from '#3p/3p';
-import {draw3p, init} from '#3p/integration-lib';
-
-import {smartadserver} from '#ads/vendors/smartadserver';
-
-init(window);
-register('smartadserver', smartadserver);
-
-window.draw3p = draw3p;
diff --git a/ads/_a4a-config.js b/ads/_a4a-config.js
index 9611f0d01f20..7eaa40fb5323 100644
--- a/ads/_a4a-config.js
+++ b/ads/_a4a-config.js
@@ -28,6 +28,7 @@ export function getA4ARegistry() {
'doubleclick': () => true,
'fake': () => true,
'nws': () => true,
+ 'smartadserver': () => true,
'valueimpression': () => true,
// TODO: Add new ad network implementation "is enabled" functions here.
// Note: if you add a function here that requires a new "import", above,
diff --git a/ads/vendors/smartadserver.js b/ads/vendors/smartadserver.js
deleted file mode 100644
index fc27c18b698f..000000000000
--- a/ads/vendors/smartadserver.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import {loadScript} from '#3p/3p';
-
-/**
- * @param {!Window} global
- * @param {!Object} data
- */
-export function smartadserver(global, data) {
- // For more flexibility, we construct the call to SmartAdServer's URL in the
- // external loader, based on the data received from the AMP tag.
- loadScript(global, 'https://ec-ns.sascdn.com/diff/js/amp.v0.js', () => {
- global.sas.callAmpAd(data);
- });
-}
diff --git a/build-system/compile/bundles.config.extensions.json b/build-system/compile/bundles.config.extensions.json
index 91c17e1b10f8..fe8097376ffc 100644
--- a/build-system/compile/bundles.config.extensions.json
+++ b/build-system/compile/bundles.config.extensions.json
@@ -102,6 +102,11 @@
"name": "amp-ad-network-nws-impl",
"version": "0.1"
},
+ {
+ "name": "amp-ad-network-smartadserver-impl",
+ "version": "0.1",
+ "latestVersion": "0.1"
+ },
{
"name": "amp-ad-network-valueimpression-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 99372bb35555..98f738ec202c 100644
--- a/build-system/test-configs/dep-check-config.js
+++ b/build-system/test-configs/dep-check-config.js
@@ -149,6 +149,7 @@ exports.rules = [
'extensions/amp-ad-network-oblivki-impl/0.1/amp-ad-network-oblivki-impl.js->extensions/amp-a4a/0.1/amp-a4a.js',
'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',
// 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/extensions/amp-ad-network-smartadserver-impl/0.1/amp-ad-network-smartadserver-impl.js b/extensions/amp-ad-network-smartadserver-impl/0.1/amp-ad-network-smartadserver-impl.js
new file mode 100644
index 000000000000..1e166ffda3e2
--- /dev/null
+++ b/extensions/amp-ad-network-smartadserver-impl/0.1/amp-ad-network-smartadserver-impl.js
@@ -0,0 +1,200 @@
+/**
+ * Copyright 2021 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {buildUrl} from '#ads/google/a4a/shared/url-builder';
+
+import {getPageLayoutBoxBlocking} from '#core/dom/layout/page-layout-box';
+import {tryParseJson} from '#core/types/object/json';
+import {includes} from '#core/types/string';
+
+import {Services} from '#service';
+
+import {dev} from '#utils/log';
+
+import {getOrCreateAdCid} from '../../../src/ad-cid';
+import {getConsentPolicyInfo} from '../../../src/consent';
+import {AmpA4A} from '../../amp-a4a/0.1/amp-a4a';
+
+/** @type {string} */
+const TAG = 'amp-ad-network-smartadserver-impl';
+
+/** @const {number} */
+const MAX_URL_LENGTH = 15360;
+
+/** @type {string} */
+const SAS_NO_AD_STR = '
';
+
+/**
+ * @const {!./shared/url-builder.QueryParameterDef}
+ * @visibleForTesting
+ */
+const TRUNCATION_PARAM = {
+ name: 'trunc',
+ value: 1,
+};
+
+/** @final */
+export class AmpAdNetworkSmartadserverImpl extends AmpA4A {
+ /**
+ * @param {!Element} element
+ */
+ constructor(element) {
+ super(element);
+ }
+
+ /** @override */
+ getAdUrl(opt_consentTuple, opt_rtcResponsesPromise) {
+ return Promise.any([
+ getConsentPolicyInfo(this.element, this.getConsentPolicy() || 'default'),
+ new Promise((resolve) => setTimeout(() => resolve(), 10)),
+ ]).then((consentString) => {
+ opt_rtcResponsesPromise = opt_rtcResponsesPromise || Promise.resolve();
+ const checkStillCurrent = this.verifyStillCurrent();
+
+ return opt_rtcResponsesPromise.then((result) => {
+ checkStillCurrent();
+ const rtc = this.getBestRtcCallout_(result);
+ const urlParams = {};
+
+ if (rtc && Object.keys(rtc).length) {
+ urlParams['hb_bid'] = rtc.hb_bidder || '';
+ urlParams['hb_cpm'] = rtc.hb_pb;
+ urlParams['hb_ccy'] = 'USD';
+ urlParams['hb_cache_id'] = rtc.hb_cache_id || '';
+ urlParams['hb_cache_host'] = rtc.hb_cache_host || '';
+ urlParams['hb_cache_path'] = rtc.hb_cache_path || '';
+ urlParams['hb_width'] = this.element.getAttribute('width');
+ urlParams['hb_height'] = this.element.getAttribute('height');
+ }
+
+ const formatId = this.element.getAttribute('data-format');
+ const tagId = 'sas_' + formatId;
+ return buildUrl(
+ (this.element.getAttribute('data-domain') ||
+ 'https://www.smartadserver.com') + '/ac',
+ {
+ 'siteid': this.element.getAttribute('data-site'),
+ 'pgid': this.element.getAttribute('data-page'),
+ 'fmtid': formatId,
+ 'tgt': this.element.getAttribute('data-target'),
+ 'tag': tagId,
+ 'out': 'amp-hb',
+ ...urlParams,
+ 'gdpr_consent': consentString,
+ 'pgDomain': this.win.top.location.hostname,
+ 'tmstp': Date.now(),
+ },
+ MAX_URL_LENGTH,
+ TRUNCATION_PARAM
+ );
+ });
+ });
+ }
+
+ /** @override */
+ isValidElement() {
+ return this.isAmpAdElement();
+ }
+
+ /** @override */
+ sendXhrRequest(adUrl) {
+ return super.sendXhrRequest(adUrl).then((response) => {
+ return response.text().then((responseText) => {
+ if (includes(responseText, SAS_NO_AD_STR)) {
+ this./*OK*/ collapse();
+ }
+ return new Response(response);
+ });
+ });
+ }
+
+ /** @override */
+ getCustomRealTimeConfigMacros_() {
+ const allowed = {
+ 'width': true,
+ 'height': true,
+ 'json': true,
+ 'data-override-width': true,
+ 'data-override-height': true,
+ 'data-multi-size': true,
+ 'data-slot': true,
+ };
+
+ return {
+ PAGEVIEWID: () => Services.documentInfoForDoc(this.element).pageViewId,
+ PAGEVIEWID_64: () =>
+ Services.documentInfoForDoc(this.element).pageViewId64,
+ HREF: () => this.win.location.href,
+ CANONICAL_URL: () =>
+ Services.documentInfoForDoc(this.element).canonicalUrl,
+ TGT: () =>
+ JSON.stringify(
+ (tryParseJson(this.element.getAttribute('json')) || {})['targeting']
+ ),
+ ADCID: (opt_timeout) =>
+ getOrCreateAdCid(
+ this.getAmpDoc(),
+ 'AMP_ECID_GOOGLE',
+ '_ga',
+ parseInt(opt_timeout, 10)
+ ),
+ ATTR: (name) => {
+ if (!allowed[name]) {
+ dev().warn(TAG, `Invalid attribute ${name}`);
+ return '';
+ } else {
+ return this.element.getAttribute(name);
+ }
+ },
+ ELEMENT_POS: () => getPageLayoutBoxBlocking(this.element).top,
+ SCROLL_TOP: () =>
+ Services.viewportForDoc(this.getAmpDoc()).getScrollTop(),
+ PAGE_HEIGHT: () =>
+ Services.viewportForDoc(this.getAmpDoc()).getScrollHeight(),
+ BKG_STATE: () => (this.getAmpDoc().isVisible() ? 'visible' : 'hidden'),
+ };
+ }
+
+ /**
+ * Chooses RTC callout with highest bid price
+ * @param {Array} rtcResponseArray
+ * @return {Object}
+ */
+ getBestRtcCallout_(rtcResponseArray) {
+ if (!rtcResponseArray) {
+ return {};
+ }
+
+ let highestOffer = {};
+ rtcResponseArray.forEach((item) => {
+ if (!item || !item.response || !item.response.targeting) {
+ return null;
+ } else if (
+ (highestOffer.hb_pb &&
+ item.response.targeting.hb_pb > highestOffer.hb_pb) ||
+ (!highestOffer.hb_pb && item.response.targeting.hb_pb > 0.0)
+ ) {
+ highestOffer = item.response.targeting;
+ }
+ });
+
+ return highestOffer;
+ }
+}
+
+AMP.extension(TAG, '0.1', (AMP) =>
+ AMP.registerElement(TAG, AmpAdNetworkSmartadserverImpl)
+);
diff --git a/extensions/amp-ad-network-smartadserver-impl/0.1/test/test-amp-ad-network-smartadserver-impl.js b/extensions/amp-ad-network-smartadserver-impl/0.1/test/test-amp-ad-network-smartadserver-impl.js
new file mode 100644
index 000000000000..a376eec91c45
--- /dev/null
+++ b/extensions/amp-ad-network-smartadserver-impl/0.1/test/test-amp-ad-network-smartadserver-impl.js
@@ -0,0 +1,421 @@
+/**
+ * Copyright 2021 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../amp-ad/0.1/amp-ad';
+import {createElementWithAttributes} from '#core/dom';
+
+import {Services} from '#service';
+
+import {AmpAdNetworkSmartadserverImpl} from '../amp-ad-network-smartadserver-impl';
+
+const realWinConfig = {
+ amp: {
+ extensions: ['amp-ad-network-smartadserver-impl'],
+ },
+ ampAdCss: true,
+ allowExternalResources: false,
+};
+
+describes.realWin('amp-ad-network-smartadserver-impl', realWinConfig, (env) => {
+ const rtcConfig = {
+ vendors: {
+ prebidappnexus: {
+ PLACEMENT_ID: 13133382,
+ ACCOUNT_ID: 101010,
+ },
+ indexexchange: {SITE_ID: 123456},
+ },
+ timeoutMillis: 500,
+ };
+
+ let element, impl, win, doc;
+
+ beforeEach(() => {
+ win = env.win;
+ doc = win.document;
+ });
+
+ describe('isValidElement', () => {
+ it('should be valid', async () => {
+ element = createElementWithAttributes(doc, 'amp-ad', {
+ width: '300',
+ height: '250',
+ type: 'smartadserver',
+ });
+ impl = new AmpAdNetworkSmartadserverImpl(element);
+
+ expect(impl.isValidElement()).to.be.true;
+ });
+ });
+
+ describe('getCustomRealTimeConfigMacros', () => {
+ it('should return correct macros', () => {
+ const macros = {
+ 'data-slot': '5678',
+ 'height': '50',
+ 'width': '200',
+ 'data-override-width': '310',
+ 'data-override-height': '260',
+ };
+ const json = {
+ targeting: {a: 123},
+ };
+
+ element = createElementWithAttributes(doc, 'amp-ad', {
+ 'width': macros['width'],
+ 'height': macros['height'],
+ 'type': 'smartadserver',
+ 'data-slot': 5678,
+ 'data-override-width': 310,
+ 'data-override-height': 260,
+ 'layout': 'fixed',
+ 'json': JSON.stringify(json),
+ 'rtc-config': JSON.stringify(rtcConfig),
+ });
+ env.win.document.body.appendChild(element);
+
+ const scrollTopValue = 100;
+ const scrollHeightValue = 700;
+ env.sandbox.stub(Services, 'viewportForDoc').callsFake(() => {
+ return {
+ getScrollTop: () => scrollTopValue,
+ getScrollHeight: () => scrollHeightValue,
+ };
+ });
+
+ impl = new AmpAdNetworkSmartadserverImpl(element, env.win.doc, win);
+ const docInfo = Services.documentInfoForDoc(element);
+ const customMacros = impl.getCustomRealTimeConfigMacros_();
+
+ expect(customMacros.PAGEVIEWID()).to.equal(docInfo.pageViewId);
+ expect(customMacros.PAGEVIEWID_64()).to.equal(docInfo.pageViewId64);
+ expect(customMacros.HREF()).to.equal(win.location.href);
+ expect(customMacros.CANONICAL_URL()).to.equal(docInfo.canonicalUrl);
+ expect(customMacros.TGT()).to.equal(JSON.stringify(json['targeting']));
+ expect(customMacros.ELEMENT_POS()).to.equal(
+ element.getBoundingClientRect().top + scrollY
+ );
+ expect(customMacros.SCROLL_TOP()).to.equal(scrollTopValue);
+ expect(customMacros.PAGE_HEIGHT()).to.equal(scrollHeightValue);
+ expect(customMacros.BKG_STATE()).to.equal(
+ impl.getAmpDoc().isVisible() ? 'visible' : 'hidden'
+ );
+ Object.keys(macros).forEach((macro) => {
+ expect(customMacros.ATTR(macro)).to.equal(macros[macro]);
+ });
+ return Promise.all([
+ customMacros.ADCID().then((adcid) => {
+ expect(adcid).to.not.be.null;
+ }),
+ ]);
+ });
+
+ it('should skip not allowed macros', () => {
+ const macros = {
+ 'width': '300',
+ 'height': '250',
+ 'json': '',
+ 'not-allowed': 'blabla',
+ };
+ element = createElementWithAttributes(doc, 'amp-ad', macros);
+ impl = new AmpAdNetworkSmartadserverImpl(element);
+ const customMacros = impl.getCustomRealTimeConfigMacros_();
+
+ expect(customMacros.TGT()).to.equal(undefined);
+ expect(customMacros.ATTR('width')).to.equal(macros['width']);
+ expect(customMacros.ATTR('height')).to.equal(macros['height']);
+ expect(customMacros.ATTR('not-allowed')).to.not.be.equal(
+ macros['not-allowed']
+ );
+ expect(customMacros.ATTR('not-allowed')).to.equal('');
+ });
+ });
+
+ describe('getAdUrl', () => {
+ it('should return proper url with vendor data', async () => {
+ element = createElementWithAttributes(doc, 'amp-ad', {
+ 'width': 300,
+ 'height': 250,
+ 'data-site': 111,
+ 'data-page': 121,
+ 'data-format': 222,
+ 'type': 'smartadserver',
+ 'rtc-config': JSON.stringify(rtcConfig),
+ });
+ doc.body.appendChild(element);
+
+ const viewer = Services.viewerForDoc(element);
+ env.sandbox.stub(viewer, 'getReferrerUrl');
+
+ const rtcResponseArray = [
+ {
+ response: {
+ targeting: {
+ 'hb_bidder': 'appnexus',
+ 'hb_cache_host': 'prebid.ams1.adnxs-simple.com',
+ 'hb_cache_id': '0cb22b3e-aa2d-4936-9039-0ec93ff67de5',
+ 'hb_cache_path': '/pbc/v1/cache',
+ 'hb_pb': '1.7',
+ 'hb_size': '300x250',
+ },
+ },
+ rtcTime: 210,
+ },
+ ];
+
+ return new AmpAdNetworkSmartadserverImpl(element)
+ .getAdUrl({}, Promise.resolve(rtcResponseArray))
+ .then((url) => {
+ expect(url).to.match(
+ /^https:\/\/www\.smartadserver\.com\/ac\?siteid=111&pgid=121&fmtid=222&tag=sas_222&out=amp-hb&hb_bid=appnexus&hb_cpm=1.7&hb_ccy=USD&hb_cache_id=0cb22b3e-aa2d-4936-9039-0ec93ff67de5&hb_cache_host=prebid.ams1.adnxs-simple.com&hb_cache_path=%2Fpbc%2Fv1%2Fcache&hb_width=300&hb_height=250&pgDomain=[a-zA-Z0-9.-]+&tmstp=[0-9]+$/
+ );
+ });
+ });
+
+ it('should return proper url while missing some vendor data', async () => {
+ element = createElementWithAttributes(doc, 'amp-ad', {
+ 'data-site': 11,
+ 'data-format': 23,
+ 'type': 'smartadserver',
+ 'rtc-config': JSON.stringify(rtcConfig),
+ });
+ doc.body.appendChild(element);
+
+ const viewer = Services.viewerForDoc(element);
+ env.sandbox.stub(viewer, 'getReferrerUrl');
+
+ const rtcResponseArray = [
+ {
+ response: {
+ 'targeting': {
+ 'hb_cache_host': 'prebid.ams1.adnxs-simple.com',
+ 'hb_cache_id': '0cb22b3e-aa2d-4936-9039-0ec93ff67de5',
+ 'hb_cache_path': '/pbc/v1/cache',
+ 'hb_pb': '0.4',
+ 'hb_size': '300x250',
+ },
+ },
+ rtcTime: 109,
+ },
+ ];
+
+ return new AmpAdNetworkSmartadserverImpl(element)
+ .getAdUrl({}, Promise.resolve(rtcResponseArray))
+ .then((url) => {
+ expect(url).to.match(
+ /^https:\/\/www\.smartadserver\.com\/ac\?siteid=11&fmtid=23&tag=sas_23&out=amp-hb&hb_cpm=0.4&hb_ccy=USD&hb_cache_id=0cb22b3e-aa2d-4936-9039-0ec93ff67de5&hb_cache_host=prebid.ams1.adnxs-simple.com&hb_cache_path=%2Fpbc%2Fv1%2Fcache&pgDomain=[a-zA-Z0-9.-]+&tmstp=[0-9]+$/
+ );
+ });
+ });
+
+ it('should return proper url with default vendor data', async () => {
+ element = createElementWithAttributes(doc, 'amp-ad', {
+ 'data-site': 11,
+ 'data-format': 23,
+ 'rtc-config': JSON.stringify(rtcConfig),
+ });
+ doc.body.appendChild(element);
+
+ const viewer = Services.viewerForDoc(element);
+ env.sandbox.stub(viewer, 'getReferrerUrl');
+
+ const rtcResponseArray = [
+ {
+ response: {
+ 'targeting': {
+ 'hb_pb': '0.8',
+ 'hb_size': '100x200',
+ },
+ },
+ rtcTime: 109,
+ },
+ ];
+
+ return new AmpAdNetworkSmartadserverImpl(element)
+ .getAdUrl({}, Promise.resolve(rtcResponseArray))
+ .then((url) => {
+ expect(url).to.match(
+ /^https:\/\/www\.smartadserver\.com\/ac\?siteid=11&fmtid=23&tag=sas_23&out=amp-hb&hb_cpm=0.8&hb_ccy=USD&pgDomain=[a-zA-Z0-9.-]+&tmstp=[0-9]+$/
+ );
+ });
+ });
+
+ it('should return proper url without vendor data', async () => {
+ element = createElementWithAttributes(doc, 'amp-ad', {
+ 'width': '100',
+ 'height': '50',
+ 'data-site': '1',
+ 'data-format': '33',
+ 'data-domain': 'https://ww7.smartadserver.com',
+ 'type': 'smartadserver',
+ });
+ doc.body.appendChild(element);
+
+ const viewer = Services.viewerForDoc(element);
+ env.sandbox.stub(viewer, 'getReferrerUrl');
+
+ return new AmpAdNetworkSmartadserverImpl(element)
+ .getAdUrl({}, Promise.resolve())
+ .then((url) => {
+ expect(url).to.match(
+ /^https:\/\/ww7\.smartadserver\.com\/ac\?siteid=1&fmtid=33&tag=sas_33&out=amp-hb&pgDomain=[a-zA-Z0-9.-]+&tmstp=[0-9]+$/
+ );
+ });
+ });
+
+ it('should return proper url with falsy callout response', async () => {
+ element = createElementWithAttributes(doc, 'amp-ad', {
+ 'data-site': 2,
+ 'data-format': 3,
+ });
+ doc.body.appendChild(element);
+
+ const viewer = Services.viewerForDoc(element);
+ env.sandbox.stub(viewer, 'getReferrerUrl');
+
+ return new AmpAdNetworkSmartadserverImpl(element)
+ .getAdUrl({}, null)
+ .then((url) => {
+ expect(url).to.match(
+ /^https:\/\/www\.smartadserver\.com\/ac\?siteid=2&fmtid=3&tag=sas_3&out=amp-hb&pgDomain=[a-zA-Z0-9.-]+&tmstp=[0-9]+$/
+ );
+ });
+ });
+ });
+
+ describe('sendXhrRequest', () => {
+ function mockXhrFor(response) {
+ return {
+ fetch: () =>
+ Promise.resolve({
+ text: () => Promise.resolve(response),
+ }),
+ };
+ }
+
+ it('should not collapse when ad response', async () => {
+ env.sandbox
+ .stub(Services, 'xhrFor')
+ .returns(
+ mockXhrFor('advertisement
')
+ );
+
+ impl = new AmpAdNetworkSmartadserverImpl(doc.createElement('amp-ad'));
+ const stub = env.sandbox.stub(impl, 'collapse');
+
+ expect(stub.notCalled).to.equal(true);
+ await impl.sendXhrRequest();
+ expect(stub.notCalled).to.equal(true);
+ });
+
+ it('should collapse when no ad response', async () => {
+ env.sandbox
+ .stub(Services, 'xhrFor')
+ .returns(mockXhrFor('
'));
+
+ impl = new AmpAdNetworkSmartadserverImpl(doc.createElement('amp-ad'));
+ const stub = env.sandbox.stub(impl, 'collapse');
+
+ expect(stub.notCalled).to.equal(true);
+ await impl.sendXhrRequest();
+ expect(stub.calledOnce).to.equal(true);
+ });
+ });
+
+ describe('getBestRtcCallout', () => {
+ beforeEach(() => {
+ impl = new AmpAdNetworkSmartadserverImpl(doc.createElement('amp-ad'));
+ });
+
+ it('should return best callout data', async () => {
+ const rtcResponseArray = [
+ {
+ response: {
+ targeting: {
+ 'hb_bidder': 'appnexus',
+ 'hb_bidder_appnexus': 'appnexus',
+ 'hb_cache_host': 'prebid.ams1.adnxs-simple.com',
+ 'hb_cache_host_appnex': 'prebid.ams1.adnxs-simple.com',
+ 'hb_cache_id': '558a891a-a532-423c-a30e-11a9caeea688',
+ 'hb_cache_id_appnexus': '558a891a-a532-423c-a30e-11a9caeea688',
+ 'hb_cache_path': '/pbc/v1/cache',
+ 'hb_cache_path_appnex': '/pbc/v1/cache',
+ 'hb_pb': '0.10',
+ 'hb_pb_appnexus': '0.10',
+ 'hb_size': '300x250',
+ 'hb_size_appnexus': '300x250',
+ },
+ },
+ rtcTime: 134,
+ callout: 'prebidappnexus',
+ },
+ {
+ response: {},
+ rtcTime: 122,
+ callout: 'criteo',
+ },
+ {
+ 'response': {
+ 'targeting': {
+ 'hb_bidder': 'indexexchange',
+ 'hb_cache_host': 'amp.casalemedia.com',
+ 'hb_cache_id': '558a891a-a532-423c-a30e-11a9caeea688',
+ 'hb_cache_path': '/amprtc/v1/cache',
+ 'hb_pb': '0.30',
+ 'hb_size': '300x250',
+ },
+ },
+ 'rtcTime': 106,
+ 'callout': 'indexexchange',
+ },
+ ];
+
+ expect(impl.getBestRtcCallout_(rtcResponseArray)).to.deep.equal(
+ rtcResponseArray[2].response.targeting
+ );
+ });
+
+ it('should return empty object when no offers', async () => {
+ const rtcResponseArray = [
+ {
+ 'response': {},
+ 'rtcTime': 92,
+ 'callout': 'prebidappnexus',
+ },
+ {
+ 'response': {},
+ 'rtcTime': 117,
+ 'callout': 'indexexchange',
+ },
+ {
+ 'response': {},
+ 'rtcTime': 131,
+ 'callout': 'criteo',
+ },
+ ];
+
+ expect(impl.getBestRtcCallout_(rtcResponseArray)).to.deep.equal({});
+ });
+
+ it('should return empty object when empty callouts array', async () => {
+ expect(impl.getBestRtcCallout_([])).to.deep.equal({});
+ });
+
+ it('should return empty object when falsy argument', async () => {
+ expect(impl.getBestRtcCallout_(null)).to.deep.equal({});
+ });
+ });
+});
diff --git a/extensions/amp-ad-network-smartadserver-impl/OWNERS b/extensions/amp-ad-network-smartadserver-impl/OWNERS
new file mode 100644
index 000000000000..d212171aeae4
--- /dev/null
+++ b/extensions/amp-ad-network-smartadserver-impl/OWNERS
@@ -0,0 +1,18 @@
+// 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',
+ },
+ {
+ name: 'smart-adserver',
+ notify: true,
+ },
+ ],
+ },
+ ],
+}
diff --git a/extensions/amp-ad-network-smartadserver-impl/amp-ad-network-smartadserver-impl-internal.md b/extensions/amp-ad-network-smartadserver-impl/amp-ad-network-smartadserver-impl-internal.md
new file mode 100644
index 000000000000..21f44b49063e
--- /dev/null
+++ b/extensions/amp-ad-network-smartadserver-impl/amp-ad-network-smartadserver-impl-internal.md
@@ -0,0 +1,62 @@
+
+
+# `amp-ad-network-smartadserver-impl`
+
+
+
+ Description
+ The Smartadserver fast fetch implementation for serving AMP ads, using ``.
+
+
+ Availability
+ Launched
+
+
+ Required Script
+ <script async custom-element="amp-ad" src="https://cdn.ampproject.org/v0/amp-ad-0.1.js"></script>
+
+
+
+## Behavior
+
+Smartadserver supports the Real Time Config (RTC) to preload configuration settings for ad placements. The RTC setup is optional.
+
+## Supported parameters
+
+Smartadserver largely uses the same tags as ``. The following are required tags for special behaviors of existing ones:
+
+- `data-site`: Site ID
+- `data-page`: Page ID
+- `data-format`: Format ID
+- `data-domain`: Ad call domain
+
+These attributes are optional:
+
+- `data-target`: Targeting string
+- `rtc-config`: Please refer to [RTC Documentation](https://github.com/ampproject/amphtml/blob/main/extensions/amp-a4a/rtc-documentation.md) for details
+
+### Example configuration
+
+```html
+
+
+```
diff --git a/extensions/amp-ad-network-smartadserver-impl/readme.md b/extensions/amp-ad-network-smartadserver-impl/readme.md
new file mode 100644
index 000000000000..c8c871e7911b
--- /dev/null
+++ b/extensions/amp-ad-network-smartadserver-impl/readme.md
@@ -0,0 +1,55 @@
+# SmartAdServer
+
+## Example
+
+### Basic call
+
+```html
+
+
+```
+
+### With targeting
+
+```html
+
+
+```
+
+## Configuration
+
+For ``, use the domain assigned to your network (e. g. www3.smartadserver.com); It can be found in Smart AdServer's config.js library (e.g., `http://www3.smartadserver.com/config.js?nwid=1234`).
+
+For semantics of configuration, please see [Smart AdServer help center](http://help.smartadserver.com/).
+
+### Supported parameters
+
+All of the parameters listed here should be prefixed with "data-" when used.
+
+| Parameter name | Description | Required |
+| -------------- | ----------------------------------- | -------- |
+| site | Your Smart AdServer Site ID | Yes |
+| page | Your Smart AdServer Page ID | Yes |
+| format | Your Smart AdServer Format ID | Yes |
+| domain | Your Smart AdServer call domain | Yes |
+| target | Your targeting string | No |
+| tag | An ID for the tag containing the ad | No |
+
+Note: If any of the required parameters is missing, the ad slot won't be filled.
diff --git a/extensions/amp-ad/amp-ad.md b/extensions/amp-ad/amp-ad.md
index ce049ffbcaa8..7cde45b7d777 100644
--- a/extensions/amp-ad/amp-ad.md
+++ b/extensions/amp-ad/amp-ad.md
@@ -451,7 +451,7 @@ See [amp-ad rules](validator-amp-ad.protoascii) in the AMP validator specificati
- [Sklik](../../ads/vendors/sklik.md)
- [SSP](../../ads/vendors/ssp.md)
- [SlimCut Media](../../ads/vendors/slimcutmedia.md)
-- [Smart AdServer](../../ads/vendors/smartadserver.md)
+- [Smart AdServer](../amp-ad-network-smartadserver-impl/readme.md)
- [smartclip](../../ads/vendors/smartclip.md)
- [SmileWanted](../../ads/vendors/smilewanted.md)
- [sogou Ad](../../ads/vendors/sogouad.md)