Skip to content

Commit

Permalink
Greenbids Bidder Adapter (#12510)
Browse files Browse the repository at this point in the history
* Greenbids Bidder adapter

* refacto to make the code easier and clearer

* refacto to make the code easier and clearer

* Alexis' review

* Alex's review part 2

* export more utils

* add test on news utils

* remove info that could lead to finger printing
  • Loading branch information
jeremy-greenbids authored Dec 17, 2024
1 parent b39d070 commit e060b74
Show file tree
Hide file tree
Showing 7 changed files with 1,579 additions and 0 deletions.
65 changes: 65 additions & 0 deletions libraries/pageInfosUtils/pageInfosUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Retrieves the referrer information from the bidder request.
*
* @param {Object} bidderRequest - The bidder request object.
* @param {Object} [bidderRequest.refererInfo] - The referer information object.
* @param {string} [bidderRequest.refererInfo.page] - The page URL of the referer.
* @returns {string} The referrer URL if available, otherwise an empty string.
*/
export function getReferrerInfo(bidderRequest) {
let ref = '';
if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page) {
ref = bidderRequest.refererInfo.page;
}
return ref;
}

/**
* Retrieves the title of the current web page.
*
* This function attempts to get the title from the top-level window's document.
* If an error occurs (e.g., due to cross-origin restrictions), it falls back to the current document.
* It first tries to get the title from the `og:title` meta tag, and if that is not available, it uses the document's title.
*
* @returns {string} The title of the current web page, or an empty string if no title is found.
*/
export function getPageTitle() {
try {
const ogTitle = window.top.document.querySelector('meta[property="og:title"]');
return window.top.document.title || (ogTitle && ogTitle.content) || '';
} catch (e) {
const ogTitle = document.querySelector('meta[property="og:title"]');
return document.title || (ogTitle && ogTitle.content) || '';
}
}

/**
* Retrieves the content of the page description meta tag.
*
* This function attempts to get the description from the top-level window's document.
* If it fails (e.g., due to cross-origin restrictions), it falls back to the current document.
* It looks for meta tags with either the name "description" or the property "og:description".
*
* @returns {string} The content of the description meta tag, or an empty string if not found.
*/
export function getPageDescription() {
try {
const element = window.top.document.querySelector('meta[name="description"]') ||
window.top.document.querySelector('meta[property="og:description"]');
return (element && element.content) || '';
} catch (e) {
const element = document.querySelector('meta[name="description"]') ||
document.querySelector('meta[property="og:description"]');
return (element && element.content) || '';
}
}

/**
* Retrieves the downlink speed of the user's network connection.
*
* @param {object} nav - The navigator object, typically `window.navigator`.
* @returns {string} The downlink speed as a string if available, otherwise an empty string.
*/
export function getConnectionDownLink(nav) {
return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : '';
}
37 changes: 37 additions & 0 deletions libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Calculates the Time to First Byte (TTFB) for the given window object.
*
* This function attempts to use the Navigation Timing Level 2 API first, and falls back to
* the Navigation Timing Level 1 API if the former is not available.
*
* @param {Window} win - The window object from which to retrieve performance timing information.
* @returns {string} The TTFB in milliseconds as a string, or an empty string if the TTFB cannot be determined.
*/
export function getTimeToFirstByte(win) {
const performance = win.performance || win.webkitPerformance || win.msPerformance || win.mozPerformance;

const ttfbWithTimingV2 = performance &&
typeof performance.getEntriesByType === 'function' &&
Object.prototype.toString.call(performance.getEntriesByType) === '[object Function]' &&
performance.getEntriesByType('navigation')[0] &&
performance.getEntriesByType('navigation')[0].responseStart &&
performance.getEntriesByType('navigation')[0].requestStart &&
performance.getEntriesByType('navigation')[0].responseStart > 0 &&
performance.getEntriesByType('navigation')[0].requestStart > 0 &&
Math.round(
performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart
);

if (ttfbWithTimingV2) {
return ttfbWithTimingV2.toString();
}

const ttfbWithTimingV1 = performance &&
performance.timing.responseStart &&
performance.timing.requestStart &&
performance.timing.responseStart > 0 &&
performance.timing.requestStart > 0 &&
performance.timing.responseStart - performance.timing.requestStart;

return ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : '';
}
238 changes: 238 additions & 0 deletions modules/greenbidsBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { getValue, logError, deepAccess, parseSizesInput, getBidIdParameter, logInfo } from '../src/utils.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { getStorageManager } from '../src/storageManager.js';
import { getHLen } from '../libraries/navigatorData/navigatorData.js';
import { getTimeToFirstByte } from '../libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js';
import { getReferrerInfo, getPageTitle, getPageDescription, getConnectionDownLink } from '../libraries/pageInfosUtils/pageInfosUtils.js';
/**
* @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
* @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
*/

const BIDDER_CODE = 'greenbids';
const GVL_ID = 1232;
const ENDPOINT_URL = 'https://hb.greenbids.ai';
export const storage = getStorageManager({ bidderCode: BIDDER_CODE });

export const spec = {
code: BIDDER_CODE,
gvlid: GVL_ID,
supportedMediaTypes: ['banner', 'video'],
/**
* Determines whether or not the given bid request is valid.
*
* @param {BidRequest} bid The bid params to validate.
* @return boolean True if this is a valid bid, and false otherwise.
*/
isBidRequestValid: function (bid) {
if (typeof bid.params !== 'undefined' && parseInt(getValue(bid.params, 'placementId')) > 0) {
logInfo('Greenbids bidder adapter valid bid request');
return true;
} else {
logError('Greenbids bidder adapter requires placementId to be defined and a positive number');
return false;
}
},
/**
* Make a server request from the list of BidRequests.
*
* @param {validBidRequests[]} validBidRequests array of bids
* @param bidderRequest bidder request object
* @return ServerRequest Info describing the request to the server.
*/
buildRequests: function (validBidRequests, bidderRequest) {
const bids = validBidRequests.map(bids => {
const reqObj = {};
let placementId = getValue(bids.params, 'placementId');
const gpid = deepAccess(bids, 'ortb2Imp.ext.gpid');
reqObj.sizes = getSizes(bids);
reqObj.bidId = getBidIdParameter('bidId', bids);
reqObj.bidderRequestId = getBidIdParameter('bidderRequestId', bids);
reqObj.placementId = parseInt(placementId, 10);
reqObj.adUnitCode = getBidIdParameter('adUnitCode', bids);
reqObj.transactionId = bids.ortb2Imp?.ext?.tid || '';
if (gpid) { reqObj.gpid = gpid; }
});
const topWindow = window.top;

const payload = {
referrer: getReferrerInfo(bidderRequest),
pageReferrer: document.referrer,
pageTitle: getPageTitle().slice(0, 300),
pageDescription: getPageDescription().slice(0, 300),
networkBandwidth: getConnectionDownLink(window.navigator),
timeToFirstByte: getTimeToFirstByte(window),
data: bids,
device: bidderRequest?.ortb2?.device || {},
deviceWidth: screen.width,
deviceHeight: screen.height,
devicePixelRatio: topWindow.devicePixelRatio,
screenOrientation: screen.orientation?.type,
historyLength: getHLen(),
viewportHeight: topWindow.visualViewport?.height,
viewportWidth: topWindow.visualViewport?.width,
prebid_version: '$prebid.version$',
};

const firstBidRequest = validBidRequests[0];

if (firstBidRequest.schain) {
payload.schain = firstBidRequest.schain;
}

hydratePayloadWithGppConsentData(payload, bidderRequest.gppConsent);
hydratePayloadWithGdprConsentData(payload, bidderRequest.gdprConsent);
hydratePayloadWithUspConsentData(payload, bidderRequest.uspConsent);

const userAgentClientHints = deepAccess(firstBidRequest, 'ortb2.device.sua');
if (userAgentClientHints) {
payload.userAgentClientHints = userAgentClientHints;
}

const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa');
if (dsa) {
payload.dsa = dsa;
}

const payloadString = JSON.stringify(payload);
return {
method: 'POST',
url: ENDPOINT_URL,
data: payloadString,
};
},
/**
* Unpack the response from the server into a list of bids.
*
* @param {*} serverResponse A successful response from the server.
* @return {Bid[]} An array of bids which were nested inside the server response.
*/
interpretResponse: function (serverResponse) {
serverResponse = serverResponse.body;
if (!serverResponse.responses) {
return [];
}
return serverResponse.responses.map((bid) => {
const bidResponse = {
cpm: bid.cpm,
width: bid.width,
height: bid.height,
currency: bid.currency,
netRevenue: true,
size: bid.size,
ttl: bid.ttl,
meta: {
advertiserDomains: bid && bid.adomain ? bid.adomain : [],
},
ad: bid.ad,
requestId: bid.bidId,
creativeId: bid.creativeId,
placementId: bid.placementId,
};
if (bid.dealId) {
bidResponse.dealId = bid.dealId
}
if (bid?.ext?.dsa) {
bidResponse.meta.dsa = bid.ext.dsa;
}
return bidResponse;
});
}
};

registerBidder(spec);

/**
* Converts the sizes from the bid object to the required format.
*
* @param {Object} bid - The bid object containing size information.
* @param {Array} bid.sizes - The sizes array from the bid object.
* @returns {Array} - The parsed sizes in the required format.
*/
function getSizes(bid) {
return parseSizesInput(bid.sizes);
}

// Privacy handling

/**
* Hydrates the given payload with GPP consent data if available.
*
* @param {Object} payload - The payload object to be hydrated.
* @param {Object} gppData - The GPP consent data object.
* @param {string} gppData.gppString - The GPP consent string.
* @param {number[]} gppData.applicableSections - An array of applicable section IDs.
*/
function hydratePayloadWithGppConsentData(payload, gppData) {
if (!gppData) { return; }
let isValidConsentString = typeof gppData.gppString === 'string';
let validateApplicableSections =
Array.isArray(gppData.applicableSections) &&
gppData.applicableSections.every((section) => typeof (section) === 'number')
payload.gpp = {
consentString: isValidConsentString ? gppData.gppString : '',
applicableSectionIds: validateApplicableSections ? gppData.applicableSections : [],
};
}

/**
* Hydrates the given payload with GDPR consent data if available.
*
* @param {Object} payload - The payload object to be hydrated with GDPR consent data.
* @param {Object} gdprData - The GDPR data object containing consent information.
* @param {boolean} gdprData.gdprApplies - Indicates if GDPR applies.
* @param {string} gdprData.consentString - The GDPR consent string.
* @param {number} gdprData.apiVersion - The version of the GDPR API being used.
* @param {Object} gdprData.vendorData - Additional vendor data related to GDPR.
*/
function hydratePayloadWithGdprConsentData(payload, gdprData) {
if (!gdprData) { return; }
let isCmp = typeof gdprData.gdprApplies === 'boolean';
let isConsentString = typeof gdprData.consentString === 'string';
let status = isCmp
? findGdprStatus(gdprData.gdprApplies, gdprData.vendorData)
: gdprStatus.CMP_NOT_FOUND_OR_ERROR;
payload.gdpr_iab = {
consent: isConsentString ? gdprData.consentString : '',
status: status,
apiVersion: gdprData.apiVersion
};
}

/**
* Adds USP (CCPA) consent data to the payload if available.
*
* @param {Object} payload - The payload object to be hydrated with USP consent data.
* @param {string} uspConsentData - The USP consent string to be added to the payload.
*/
function hydratePayloadWithUspConsentData(payload, uspConsentData) {
if (!uspConsentData) { return; }
payload.us_privacy = uspConsentData;
}

const gdprStatus = {
GDPR_APPLIES_PUBLISHER: 12,
GDPR_APPLIES_GLOBAL: 11,
GDPR_DOESNT_APPLY: 0,
CMP_NOT_FOUND_OR_ERROR: 22
};

/**
* Determines the GDPR status based on whether GDPR applies and the provided GDPR data.
*
* @param {boolean} gdprApplies - Indicates if GDPR applies.
* @param {Object} gdprData - The GDPR data object.
* @param {boolean} gdprData.isServiceSpecific - Indicates if the GDPR data is service-specific.
* @returns {string} The GDPR status.
*/
function findGdprStatus(gdprApplies, gdprData) {
let status = gdprStatus.GDPR_APPLIES_PUBLISHER;
if (gdprApplies) {
if (gdprData && !gdprData.isServiceSpecific) {
status = gdprStatus.GDPR_APPLIES_GLOBAL;
}
} else {
status = gdprStatus.GDPR_DOESNT_APPLY;
}
return status;
}
32 changes: 32 additions & 0 deletions modules/greenbidsBidAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Overview

**Module Name**: Greenbids Bidder Adapter
**Module Type**: Bidder Adapter
**Maintainer**: [email protected]

# Description

Use `greenbids` as bidder.

## AdUnits configuration example
```
var adUnits = [{
code: 'your-slot_1-div', //use exactly the same code as your slot div id.
sizes: [[300, 250]],
bids: [{
bidder: 'greenbids',
params: {
placementId: 12345,
}
}]
},{
code: 'your-slot_2-div', //use exactly the same code as your slot div id.
sizes: [[600, 800]],
bids: [{
bidder: 'greenbids',
params: {
placementId: 12345,
}
}]
}];
```
Loading

0 comments on commit e060b74

Please sign in to comment.