Skip to content

EX.CO Adapter: viewability measurement, sync, bid won and timeout pixels support #13047

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
222 changes: 217 additions & 5 deletions modules/excoBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js';
import { VIDEO, BANNER } from '../src/mediaTypes.js';
import { ortbConverter } from '../libraries/ortbConverter/converter.js';
import { pbsExtensions } from '../libraries/pbsExtensions/pbsExtensions.js';
import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js';
import {
mergeDeep,
deepAccess,
Expand All @@ -17,16 +18,24 @@ import {
} from '../src/utils.js';

export const SID = window.excoPid || generateUUID();
export const ENDPOINT = '//v.ex.co/se/openrtb/hb/pbjs';
const SYNC_URL = '//cdn.ex.co/sync/e15e216-l/cookie_sync.html';
export const ENDPOINT = 'https://v.ex.co/se/openrtb/hb/pbjs';
const SYNC_URL = 'https://cdn.ex.co/sync/e15e216-l/cookie_sync.html';

export const BIDDER_CODE = 'exco';
const VERSION = '0.0.1';
const VERSION = '0.0.2';
const CURRENCY = 'USD';

const SYNC = {
done: false,
};

const EVENTS = {
TYPE: 'exco-adapter',
PING: 'exco-adapter-ping',
PONG: 'exco-adapter-pong',
subscribed: false,
};

export class AdapterHelpers {
doSync(gdprConsent = { consentString: '', gdprApplies: false }, accountId) {
insertUserSyncIframe(
Expand Down Expand Up @@ -117,6 +126,7 @@ export class AdapterHelpers {

bidResponse.ad = bid.ad;
bidResponse.adUrl = bid.adUrl;
bidResponse.nurl = bid.nurl;

bidResponse.mediaType = bid.mediaType || VIDEO;
bidResponse.meta.mediaType = bid.mediaType || VIDEO;
Expand All @@ -140,6 +150,143 @@ export class AdapterHelpers {
return bidResponse;
}

replaceMacro(str) {
return str.replace('[TIMESTAMP]', Date.now());
}

percentInViewport(win, element) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think you can import all this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have not found proper util to fit our needs

if (!element) {
return 0;
}

const rect = getBoundingClientRect(element);
const viewportHeight = win.innerHeight || win.document.documentElement.clientHeight;
const viewportWidth = win.innerWidth || win.document.documentElement.clientWidth;

// Calculate boundaries of intersection between element and viewport.
const visibleRect = {
top: Math.max(rect.top, 0),
left: Math.max(rect.left, 0),
bottom: Math.min(rect.bottom, viewportHeight),
right: Math.min(rect.right, viewportWidth)
};

// Compute intersection dimensions.
const intersectionWidth = visibleRect.right - visibleRect.left;
const intersectionHeight = visibleRect.bottom - visibleRect.top;

// If no visible area, return 0.
if (intersectionWidth <= 0 || intersectionHeight <= 0) {
return 0;
}

const intersectionArea = intersectionWidth * intersectionHeight;
const elementArea = rect.width * rect.height;

return (intersectionArea / elementArea) * 100;
}

postToAllParentFrames = (message) => {
window.parent.postMessage(message, '*');

for (let i = 0; i < window.parent.frames.length; i++) {
window.parent.frames[i].postMessage(message, '*');
}
}

sendMessage(eventName, data = {}) {
this.postToAllParentFrames({
type: EVENTS.TYPE,
eventName,
metadata: data
});
}

listenForMessages() {
window.addEventListener('message', ({ data }) => {
if (data && data.type === EVENTS.TYPE && data.eventName === EVENTS.PING) {
const { href, sid } = data.metadata;

if (href) {
const frame = document.querySelector(`iframe[src*="${href}"]`);

if (frame) {
const viewPercent = this.percentInViewport(window, frame);

this.sendMessage(EVENTS.PONG, {
viewPercent, sid
});
}
}
}
});
}

getEventUrl(data, eventName) {
const bid = data[0];
const params = {
adapterVersion: VERSION,
prebidVersion: '$prebid.version$',
pageLoadUid: SID,
eventName,
extraData: {
timepassed: bid.metrics.timeSince('requestBids'),
}
};

if (bid) {
params.adUnitCode = bid.adUnitCode;
params.auctionId = bid.auctionId;
params.bidId = bid.bidId;
params.bidderRequestId = bid.bidderRequestId;
params.bidderRequestsCount = bid.bidderRequestsCount;
params.bidderWinsCount = bid.bidderWinsCount;
params.maxBidderCalls = bid.maxBidderCalls;
params.transactionId = bid.transactionId;

if (bid.params && bid.params[0]) {
params.publisherId = bid.params[0].publisherId;
params.accountId = bid.params[0].accountId;
params.tagId = bid.params[0].tagId;
}

if (bid.ortb2.device) {
params.width = bid.ortb2.device.w;
params.height = bid.ortb2.device.h;
}

if (bid.ortb2.site) {
params.domain = bid.ortb2.site.domain;
params.parentUrl = bid.ortb2.site.page;
params.parentReferrer = bid.ortb2.site.referrer;
}

if (bid.ortb2.app) {
params.environment = 'app';
}
}

const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (typeof value === 'object' && Array.isArray(value) === false) {
Object.entries(value).forEach(([k, v]) => {
searchParams.append(`${key}.${k}`, v);
});
} else if (value !== undefined) {
searchParams.append(key, value);
}
});

return `https://v.ex.co/event?${searchParams}`;
}

triggerUrl(url) {
fetch(url, {
keepalive: true,
credentials: 'include'
});
}

log(severity, message) {
const msg = `${BIDDER_CODE.toUpperCase()}: ${message}`;

Expand Down Expand Up @@ -278,7 +425,15 @@ export const spec = {
*/
interpretResponse: function (response, request) {
const body = response?.body?.Result || response?.body || {};
return converter.fromORTB({response: body, request: request?.data}).bids || [];
const converted = converter.fromORTB({response: body, request: request?.data});
const bids = converted.bids || [];

if (bids.length && !EVENTS.subscribed) {
EVENTS.subscribed = true;
helpers.listenForMessages();
}

return bids;
},

/**
Expand All @@ -294,12 +449,69 @@ export const spec = {
gdprConsent,
uspConsent
) {
const result = [];

const collectSyncs = (syncs) => {
if (syncs) {
syncs.forEach(sync => {
if (syncOptions.iframeEnabled && sync.type === 'iframe') {
result.push({ type: sync.type, url: sync.url });
} else if (syncOptions.pixelEnabled && ['image', 'pixel'].includes(sync.type)) {
result.push({ type: 'image', url: sync.url });
}
});
}
}

serverResponses.forEach(response => {
const { body = {} } = response;
const { ext } = body;

if (ext && ext.syncs) {
collectSyncs(ext.syncs);
}

if (ext && ext.usersync) {
Object.keys(ext.usersync).forEach(key => {
collectSyncs(ext.usersync[key].syncs);
});
}
});

if (syncOptions.iframeEnabled && !SYNC.done) {
helpers.doSync(gdprConsent);
SYNC.done = true;
}

return [];
return result;
},

/**
* Register bidder specific code, which will execute if bidder timed out after an auction
* @param {Object} data - Contains timeout specific data
*/
onTimeout: function (data) {
const eventUrl = helpers.getEventUrl(data, 'mcd_bidder_auction_timeout');

if (eventUrl) {
helpers.triggerUrl(eventUrl);
}
},

/**
* Register bidder specific code, which will execute if a bid from this bidder won the auction
* @param {import('../src/auction.js').BidResponse} bid - The bid that won the auction
*/
onBidWon: function (bid) {
if (bid == null) {
return;
}

if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) {
helpers.triggerUrl(
helpers.replaceMacro(bid.nurl)
);
}
},
};

Expand Down
Loading