diff --git a/modules.json b/modules.json index 7631d7395e3..bf1d7d9c298 100644 --- a/modules.json +++ b/modules.json @@ -5,6 +5,7 @@ "appnexusBidAdapter", "audienceNetworkBidAdapter", "conversantBidAdapter", + "criteoBidAdapter", "freestarAnalyticsAdapter", "gumgumBidAdapter", "justpremiumBidAdapter", diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js new file mode 100644 index 00000000000..0b0e98ea057 --- /dev/null +++ b/modules/criteoBidAdapter.js @@ -0,0 +1,281 @@ +import { loadScript } from 'src/adloader'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { EVENTS } from 'src/constants'; +import { parse } from 'src/url'; +import * as utils from 'src/utils'; + +var events = require('src/events'); + +const ADAPTER_VERSION = 4; +const BIDDER_CODE = 'criteo'; +const CDB_ENDPOINT = '//bidder.criteo.com/cdb'; +const INTEGRATION_MODES = { + 'amp': 1, +}; +const PROFILE_ID = 207; + +// Unminified source code can be found in: https://github.com/Prebid-org/prebid-js-external-js-criteo/blob/master/dist/prod.js +const PUBLISHER_TAG_URL = '//static.criteo.net/js/ld/publishertag.prebid.js'; + +/** @type {BidderSpec} */ +export const spec = { + code: BIDDER_CODE, + + /** + * @param {object} bid + * @return {boolean} + */ + isBidRequestValid: bid => ( + !!(bid && bid.params && (bid.params.zoneId || bid.params.networkId)) + ), + + /** + * @param {BidRequest[]} bidRequests + * @param {*} bidderRequest + * @return {ServerRequest}x` + */ + buildRequests: (bidRequests, bidderRequest) => { + let url; + let data; + + // If publisher tag not already loaded try to get it from fast bid + if (!publisherTagAvailable()) { + window.Criteo = window.Criteo || {}; + window.Criteo.usePrebidEvents = false; + + tryGetCriteoFastBid(); + + // Reload the PublisherTag after the timeout to ensure FastBid is up-to-date and tracking done properly + setTimeout(() => { + loadScript(PUBLISHER_TAG_URL); + }, bidderRequest.timeout); + } + + if (publisherTagAvailable()) { + const adapter = new Criteo.PubTag.Adapters.Prebid(PROFILE_ID, ADAPTER_VERSION, bidRequests, bidderRequest); + url = adapter.buildCdbUrl(); + data = adapter.buildCdbRequest(); + + registerEventHandlers(); + } else { + const context = buildContext(bidRequests); + url = buildCdbUrl(context); + data = buildCdbRequest(context, bidRequests); + } + + if (data) { + return { method: 'POST', url, data, bidRequests }; + } + }, + + /** + * @param {*} response + * @param {ServerRequest} request + * @return {Bid[]} + */ + interpretResponse: (response, request) => { + const body = response.body || response; + + if (publisherTagAvailable()) { + const adapter = Criteo.PubTag.Adapters.Prebid.GetAdapter(request); + if (adapter) { + return adapter.interpretResponse(body, request); + } + } + + const bids = []; + + if (body && body.slots && utils.isArray(body.slots)) { + body.slots.forEach(slot => { + const bidRequest = request.bidRequests.find(b => b.adUnitCode === slot.impid); + const bidId = bidRequest.bidId; + const bid = { + requestId: bidId, + cpm: slot.cpm, + currency: slot.currency, + netRevenue: true, + ttl: slot.ttl || 60, + creativeId: bidId, + width: slot.width, + height: slot.height, + } + if (slot.native) { + bid.ad = createNativeAd(bidId, slot.native, bidRequest.params.nativeCallback); + } else { + bid.ad = slot.creative; + } + bids.push(bid); + }); + } + + return bids; + }, +}; + +/** + * @return {boolean} + */ +function publisherTagAvailable() { + return typeof Criteo !== 'undefined' && Criteo.PubTag && Criteo.PubTag.Adapters && Criteo.PubTag.Adapters.Prebid; +} + +/** + * @param {BidRequest[]} bidRequests + * @return {CriteoContext} + */ +function buildContext(bidRequests) { + const url = utils.getTopWindowUrl(); + const queryString = parse(url).search; + + const context = { + url: url, + debug: queryString['pbt_debug'] === '1', + noLog: queryString['pbt_nolog'] === '1', + integrationMode: undefined, + }; + + bidRequests.forEach(bidRequest => { + if (bidRequest.params.integrationMode) { + context.integrationMode = bidRequest.params.integrationMode; + } + }) + + return context; +} + +/** + * @param {CriteoContext} context + * @return {string} + */ +function buildCdbUrl(context) { + let url = CDB_ENDPOINT; + url += '?profileId=' + PROFILE_ID; + url += '&av=' + String(ADAPTER_VERSION); + url += '&cb=' + String(Math.floor(Math.random() * 99999999999)); + + if (context.integrationMode in INTEGRATION_MODES) { + url += '&im=' + INTEGRATION_MODES[context.integrationMode]; + } + if (context.debug) { + url += '&debug=1'; + } + if (context.noLog) { + url += '&nolog=1'; + } + + return url; +} + +/** + * @param {CriteoContext} context + * @param {BidRequest[]} bidRequests + * @return {*} + */ +function buildCdbRequest(context, bidRequests) { + let networkId; + const request = { + publisher: { + url: context.url, + }, + slots: bidRequests.map(bidRequest => { + networkId = bidRequest.params.networkId || networkId; + const slot = { + impid: bidRequest.adUnitCode, + transactionid: bidRequest.transactionId, + auctionId: bidRequest.auctionId, + sizes: bidRequest.sizes.map(size => size[0] + 'x' + size[1]), + }; + if (bidRequest.params.zoneId) { + slot.zoneid = bidRequest.params.zoneId; + } + if (bidRequest.params.publisherSubId) { + slot.publishersubid = bidRequest.params.publisherSubId; + } + if (bidRequest.params.nativeCallback) { + slot.native = true; + } + return slot; + }), + }; + if (networkId) { + request.publisher.networkid = networkId; + } + return request; +} + +/** + * @param {string} id + * @param {*} payload + * @param {*} callback + * @return {string} + */ +function createNativeAd(id, payload, callback) { + // Store the callback and payload in a global object to be later accessed from the creative + window.criteo_prebid_native_slots = window.criteo_prebid_native_slots || {}; + window.criteo_prebid_native_slots[id] = { callback, payload }; + + // The creative is in an iframe so we have to get the callback and payload + // from the parent window (doesn't work with safeframes) + return ``; +} + +/** + * @return {boolean} + */ +function tryGetCriteoFastBid() { + try { + const fastBid = localStorage.getItem('criteo_fast_bid'); + if (fastBid !== null) { + eval(fastBid); // eslint-disable-line no-eval + return true; + } + } catch (e) { + // Unable to get fast bid + } + return false; +} + +// Register events to know when Criteo won the bid or timeouted +let registeredEvents = false; +function registerEventHandlers() { + if (registeredEvents) { + return; + } + + registeredEvents = true; + + events.on(EVENTS.BID_WON, (bid) => { + if (bid.bidderCode === 'criteo') { + const adapter = Criteo.PubTag.Adapters.Prebid.GetAdapter(bid.auctionId); + adapter.handleBidWon(bid); + } + }); + + events.on(EVENTS.BID_TIMEOUT, (timedOutBidders) => { + timedOutBidders + .filter(bidder => bidder.bidder === 'criteo') + .map(bidder => { + const adapter = Criteo.PubTag.Adapters.Prebid.GetAdapter(bidder.auctionId); + adapter.handleBidTimeout(); + }); + }); + + events.on(EVENTS.SET_TARGETING, () => { + const adapters = Criteo.PubTag.Adapters.Prebid.GetAllAdapters(); + for (const k in adapters) { + adapters[k].handleSetTargeting(); + } + }); +} + +registerBidder(spec);