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 385948f1a3c1..a29eb230fe3c 100644 --- a/build-system/compile/bundles.config.extensions.json +++ b/build-system/compile/bundles.config.extensions.json @@ -112,6 +112,11 @@ "version": "0.1", "latestVersion": "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 0fb0d895883b..8d159861866c 100644 --- a/build-system/test-configs/dep-check-config.js +++ b/build-system/test-configs/dep-check-config.js @@ -152,6 +152,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` + + + + + + + + + + + + + + +
DescriptionThe Smartadserver fast fetch implementation for serving AMP ads, using ``.
AvailabilityLaunched
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 b4add31104f4..04f84f273eb0 100644 --- a/extensions/amp-ad/amp-ad.md +++ b/extensions/amp-ad/amp-ad.md @@ -449,7 +449,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)