import { getConnectionInfo, getConnectionType } from '../libraries/connectionInfo/connectionUtils.js'; import { getDevicePixelRatio } from '../libraries/devicePixelRatio/devicePixelRatio.js'; import { getDNT } from '../libraries/dnt/index.js'; import { createMgidSessionStorage } from '../libraries/mgidUtils/mgidSessionStorage.js'; import { getUserSyncs } from '../libraries/mgidUtils/mgidUtils.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { parseUserAgentDetailed } from '../libraries/userAgentUtils/detailed.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { bidderSettings } from '../src/bidderSettings.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { config } from '../src/config.js'; import { NATIVE_ASSET_TYPES, NATIVE_IMAGE_TYPES } from '../src/constants.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { toOrtbNativeRequest } from '../src/native.js'; import { getStorageManager } from '../src/storageManager.js'; import { deepAccess, deepSetValue, getBidIdParameter, isArray, isEmpty, isFn, isInteger, isNumber, isPlainObject, isStr, logInfo, logWarn, setOnAny, triggerNurlWithCpm, triggerPixel, } from '../src/utils.js'; /** * @typedef {import('../src/adapterManager.js').BidRequest} BidRequest * @typedef {import('../src/bidfactory.js').Bid} Bid * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse * @typedef {import('./mgidBidAdapter.d.ts').MgidBidderParams} MgidBidderParams * @typedef {BidRequest & { params: MgidBidderParams }} MgidBidRequest */ const GVLID = 358; const DEFAULT_CUR = 'USD'; const BIDDER_CODE = 'mgid'; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); export const mgidSession = createMgidSessionStorage(storage); const ENDPOINT_URL = 'https://prebid.mgid.com/prebid/'; const LOG_WARN_PREFIX = '[MGID warn]: '; const LOG_INFO_PREFIX = '[MGID info]: '; const DEFAULT_IMAGE_WIDTH = 492; const DEFAULT_IMAGE_HEIGHT = 328; const DEFAULT_ICON_WIDTH = 50; const DEFAULT_ICON_HEIGHT = 50; const LS_KEY_MUID = 'mgMuidn'; // Supported native asset keys for isBidRequestValid const SUPPORTED_NATIVE_KEYS = new Set([ 'title', 'image', 'icon', 'sponsoredBy', 'data', 'price', 'saleprice', 'salePrice', 'displayurl', 'displayUrl', 'cta', 'body', 'sponsored', ]); const converter = ortbConverter({ context: { netRevenue: true, ttl: 1800, }, imp(buildImp, bidRequest, context) { const imp = buildImp(bidRequest, context); imp.tagid = bidRequest.adUnitCode; imp.secure = window.location.protocol === 'https:' ? 1 : 0; imp.displaymanager = 'Prebid.js'; imp.displaymanagerver = '$prebid.version$'; const floorData = getBidFloor(bidRequest, context.currency || DEFAULT_CUR); if (floorData.floor) { imp.bidfloor = floorData.floor; if (floorData.cur) { imp.bidfloorcur = floorData.cur; } else { delete imp.bidfloorcur; } } const legacyNative = normalizeLegacyNativeParams(bidRequest.nativeParams || deepAccess(bidRequest, 'mediaTypes.native')); const nativeReq = bidRequest.nativeOrtbRequest || (legacyNative ? toOrtbNativeRequest(legacyNative) : null); if (nativeReq) { try { populateNativeImp(imp, nativeReq); } catch (e) { logWarn(LOG_WARN_PREFIX + `populateNativeImp failed: ${e.message}`); delete imp.native; if (!imp.banner) { return null; } } } const { id: storageId, type: storageIdType } = resolveStorageId(bidRequest); if (storageId && context.enhancedBidData && isViewabilityEnabled()) { const viewrate = mgidSession.getViewrate(storageId); if (isStr(viewrate) && viewrate.length > 0) { deepSetValue(imp, 'ext.data.viewrate1w', viewrate); deepSetValue(imp, 'ext.data.viewrateIdType', storageIdType); } } return imp; }, request(buildRequest, imps, bidderRequest, context) { const validImps = imps.filter(imp => imp != null); if (validImps.length === 0) { return; } const request = buildRequest(validImps, bidderRequest, context); populateRequest(request, context); logInfo(LOG_INFO_PREFIX + `buildRequest:`, request); return request; }, bidResponse(buildBidResponse, bid, context) { if (bid.mtype === 4 || deepAccess(bid, 'ext.crtype') === 'native') { context.mediaType = NATIVE; if (isStr(bid.adm)) { try { const parsed = JSON.parse(bid.adm); if (isPlainObject(parsed.native) && !isArray(parsed.assets)) { bid = { ...bid, adm: parsed.native }; } } catch (e) {} } else if (isPlainObject(bid.adm) && isPlainObject(bid.adm.native) && !isArray(bid.adm.assets)) { bid = { ...bid, adm: bid.adm.native }; } } else if (bid.mtype === 1 || !bid.mtype) { context.mediaType = BANNER; } const bidResponse = buildBidResponse(bid, context); bidResponse.nurl = bid.nurl || ''; bidResponse.burl = bid.burl || ''; bidResponse.isBurl = isStr(bid.burl) && bid.burl.length > 0; if (context.enhancedBidData) { const { id: viewrateId } = resolveStorageId(context.bidRequest); if (viewrateId) { bidResponse.mgVRID = viewrateId; } } if (bid.exp) { bidResponse.ttl = bid.exp; } else if (bid.ttl) { bidResponse.ttl = bid.ttl; } const { seatbid } = context; if (isStr(seatbid?.seat)) { bidResponse.meta = bidResponse.meta || {}; bidResponse.meta.seat = seatbid.seat; } if (bidResponse.mediaType === NATIVE && isEmpty(deepAccess(bidResponse, 'native.ortb.assets'))) { return; } return bidResponse; }, response(buildResponse, bidResponses, ortbResponse, context) { const muidn = deepAccess(ortbResponse, 'ext.muidn'); if (isStr(muidn) && muidn.length > 0) { setLocalStorageSafely(LS_KEY_MUID, muidn); } return buildResponse(bidResponses, ortbResponse, context); }, }); /** @type {BidderSpec & { VERSION: string }} */ export const spec = { VERSION: '2.0', code: BIDDER_CODE, gvlid: GVLID, supportedMediaTypes: [BANNER, NATIVE], reId: /^[1-9][0-9]*$/, /** * Determines whether or not the given bid request is valid. * * @param {MgidBidRequest} bid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: (bid) => { const banner = deepAccess(bid, 'mediaTypes.banner'); const native = deepAccess(bid, 'mediaTypes.native'); let nativeOk = isPlainObject(native); if (nativeOk) { const nativeParams = deepAccess(bid, 'nativeParams'); const ortbAssets = deepAccess(bid, 'nativeParams.ortb.assets') || deepAccess(bid, 'mediaTypes.native.ortb.assets'); if (isArray(ortbAssets) && ortbAssets.length > 0) { nativeOk = true; } else if (isPlainObject(nativeParams)) { let assetsCount = 0; for (const k in nativeParams) { const v = nativeParams[k]; const supportProp = SUPPORTED_NATIVE_KEYS.has(k); if (supportProp) { assetsCount++; } if (!isPlainObject(v) || (!supportProp && deepAccess(v, 'required'))) { nativeOk = false; break; } } nativeOk = nativeOk && (assetsCount > 0); } else { nativeOk = false; } } let bannerOk = isPlainObject(banner); if (bannerOk) { const sizes = deepAccess(banner, 'sizes'); bannerOk = isArray(sizes) && sizes.length > 0; for (let f = 0; bannerOk && f < sizes.length; f++) { bannerOk = sizes[f].length === 2; } } const acc = Number(bid.params.accountId); return (bannerOk || nativeOk) && isPlainObject(bid.params) && !!bid.adUnitCode && isStr(bid.adUnitCode) && !!acc && acc > 0 && bid.params.accountId.toString().search(spec.reId) === 0; }, /** * Make a server request from the list of BidRequests. * * @param {MgidBidRequest[]} validBidRequests A non-empty list of bid requests which should be sent to the Server. * @param bidderRequest * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { logInfo(LOG_INFO_PREFIX + `buildRequests`); if (validBidRequests.length === 0) { return; } const accountId = setOnAny(validBidRequests, 'params.accountId'); const muid = getLocalStorageSafely(LS_KEY_MUID); let url = (setOnAny(validBidRequests, 'params.bidUrl') || ENDPOINT_URL) + accountId; if (isStr(muid) && muid.length > 0) { url += (url.indexOf('?') > -1 ? '&' : '?') + 'muid=' + encodeURIComponent(muid); } const currency = setOnAny(validBidRequests, 'params.currency') || setOnAny(validBidRequests, 'params.cur') || deepAccess(bidderRequest, 'ortb2.ext.prebid.adServerCurrency') || DEFAULT_CUR; const data = converter.toORTB({ bidRequests: validBidRequests, bidderRequest, context: { currency, enhancedBidData: isEnhancedBidDataEnabled() }, }); if (!data) { return; } return { method: 'POST', url: url, data: JSON.stringify(data), ortbRequest: data, }; }, /** * Unpack the response from the server into a list of bids. * * @param {ServerResponse} serverResponse A successful response from the server. * @param request * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: (serverResponse, request) => { logInfo(LOG_INFO_PREFIX + `interpretResponse`, serverResponse); if (!serverResponse?.body?.seatbid?.length) { return []; } const { bids } = converter.fromORTB({ response: serverResponse.body, request: request.ortbRequest }); logInfo(LOG_INFO_PREFIX + `interpretedResponse`, bids); return bids; }, onBidWon: (bid) => { const cpm = deepAccess(bid, 'adserverTargeting.hb_pb') || ''; triggerNurlWithCpm(bid, cpm); if (bid.isBurl) { if (bid.mediaType === BANNER) { bid.ad = bid.ad.replace(/\${AUCTION_PRICE}/, cpm); } else { bid.burl = bid.burl.replace(/\${AUCTION_PRICE}/, cpm); triggerPixel(bid.burl); } } logInfo(LOG_INFO_PREFIX + `onBidWon`); }, onAdRenderSucceeded: (bid) => { const trackId = bid && bid.mgVRID; if (isStr(trackId) && trackId.length > 0 && isViewabilityEnabled()) { mgidSession.trackRender(trackId); logInfo(LOG_INFO_PREFIX + `onAdRenderSucceeded`); } }, onBidViewable: (bid) => { const trackId = bid && bid.mgVRID; if (isStr(trackId) && trackId.length > 0) { mgidSession.trackView(trackId); logInfo(LOG_INFO_PREFIX + `onBidViewable`); } }, getUserSyncs: getUserSyncs, }; registerBidder(spec); const LEGACY_NATIVE_ALIASES = { sponsored: 'sponsoredBy', saleprice: 'salePrice', displayurl: 'displayUrl', data: 'body', }; function normalizeLegacyNativeParams(params) { if (!params) { return params; } let normalized = Object.assign({}, params); for (const alias of Object.keys(LEGACY_NATIVE_ALIASES)) { const canonical = LEGACY_NATIVE_ALIASES[alias]; if (params[alias] && !params[canonical]) { normalized[canonical] = normalized[alias]; delete normalized[alias]; } } return normalized; } function populateNativeImp(imp, nativeReq) { if (!nativeReq) { return; } nativeReq.context = nativeReq.context || 1; nativeReq.plcmttype = nativeReq.plcmttype || 11; nativeReq.plcmtcnt = nativeReq.plcmtcnt || 1; nativeReq.privacy = nativeReq.privacy || 1; if (!nativeReq.eventtrackers) { nativeReq.eventtrackers = [ { event: 1, methods: [1, 2] }, { event: 2, methods: [1] }, ]; } let hasTitle = false; let hasImage = false; let hasSponsor = false; if (isArray(nativeReq.assets)) { nativeReq.assets.forEach(asset => { if (asset.title) { hasTitle = true; } if (asset.img) { const isIcon = asset.img.type === NATIVE_IMAGE_TYPES.ICON; if (!isIcon) { hasImage = true; } if (!asset.img.w) { asset.img.w = isIcon ? DEFAULT_ICON_WIDTH : DEFAULT_IMAGE_WIDTH; } if (!asset.img.h) { asset.img.h = isIcon ? DEFAULT_ICON_HEIGHT : DEFAULT_IMAGE_HEIGHT; } } if (asset.data && asset.data.type === NATIVE_ASSET_TYPES.sponsored) { hasSponsor = true; } }); } if (!hasTitle || !hasImage || !hasSponsor) { logWarn(LOG_WARN_PREFIX + 'Native request missing required assets (title, image, sponsoredBy)'); throw new Error('Native request missing required assets (title, image, sponsoredBy)'); } imp.native = { ver: '1.2', request: JSON.stringify(nativeReq), }; } function populateRequest(request, context) { deepSetValue(request, 'ext.mgid_ver', spec.VERSION); if (!request.cur) { request.cur = [context.currency]; } if (!isInteger(deepAccess(request, 'source.fd'))) { deepSetValue(request, 'source.fd', 1); } if (!request.device) { request.device = {}; } if (!isInteger(deepAccess(request.device, 'dnt'))) { request.device.dnt = getDNT() ? 1 : 0; } if (!isInteger(deepAccess(request.device, 'js'))) { request.device.js = 1; } if (!isNumber(deepAccess(request.device, 'pxratio'))) { let topWindow; try { topWindow = window.top; } catch (e) { topWindow = window; } deepSetValue(request, 'device.pxratio', getDevicePixelRatio(topWindow)); } if (!isInteger(deepAccess(request.device, 'connectiontype'))) { const connType = getConnectionType(); if (connType > 0) { deepSetValue(request, 'device.connectiontype', connType); } } if (!isInteger(deepAccess(request.device, 'geo.utcoffset'))) { const existingGeo = deepAccess(request.device, 'geo') || {}; deepSetValue(request, 'device.geo', { ...existingGeo, utcoffset: new Date().getTimezoneOffset() * -1 + 0 }); } const accountId = setOnAny(context.bidRequests, 'params.accountId'); if (!isStr(deepAccess(request, 'site.publisher.id'))) { deepSetValue(request, 'site.publisher.id', String(accountId)); } if (context.enhancedBidData) { mgidSession.calculatePageSession(); } const uaInfo = parseUserAgentDetailed(); if (!isInteger(deepAccess(request, 'device.devicetype'))) { deepSetValue(request, 'device.devicetype', uaInfo.devicetype); } if (!isStr(deepAccess(request, 'device.os'))) { const suaBrand = deepAccess(request, 'device.sua.platform.brand'); if (isStr(suaBrand)) { deepSetValue(request, 'device.os', suaBrand); } else if (uaInfo.os !== 'unknown') { deepSetValue(request, 'device.os', uaInfo.os); if (!isStr(deepAccess(request, 'device.osv')) && uaInfo.osv !== 'other') { deepSetValue(request, 'device.osv', uaInfo.osv); } } } if (!isStr(deepAccess(request, 'device.osv'))) { const suaVersion = deepAccess(request, 'device.sua.platform.version'); if (isArray(suaVersion) && suaVersion.length > 0) { deepSetValue(request, 'device.osv', suaVersion.join('.')); } } if (!isStr(deepAccess(request, 'device.model'))) { const model = deepAccess(request, 'device.sua.model'); if (isStr(model)) { deepSetValue(request, 'device.model', model); } } deepSetValue(request, 'user.ext.mgid.enhanced', context.enhancedBidData ? 1 : 0); if (context.enhancedBidData) { populateMgidData(request); } populateConnectionInfo(request); // backward compat: params.bcat/badv/wlang used to be supported directly; ortb2 takes priority if (!isArray(request.bcat) || request.bcat.length === 0) { const bcat = setOnAny(context.bidRequests, 'params.bcat'); if (isArray(bcat) && bcat.length > 0) { request.bcat = bcat; } } if (!isArray(request.badv) || request.badv.length === 0) { const badv = setOnAny(context.bidRequests, 'params.badv'); if (isArray(badv) && badv.length > 0) { request.badv = badv; } } if (!isArray(request.wlang) || request.wlang.length === 0) { const wlang = setOnAny(context.bidRequests, 'params.wlang'); if (isArray(wlang) && wlang.length > 0) { request.wlang = wlang; } } } function populateMgidData(request) { const pvid = mgidSession.getOrCreatePvid(); if (isStr(pvid) && pvid.length > 0) { deepSetValue(request, 'user.ext.mgid.pvid', pvid); } const sessionInfo = mgidSession.getSessionInfo(); if (isStr(sessionInfo.sid) && sessionInfo.sid.length > 0) { deepSetValue(request, 'user.ext.mgid.sid', sessionInfo.sid); } if (isNumber(sessionInfo.sessionPage) && sessionInfo.sessionPage > 0) { deepSetValue(request, 'user.ext.mgid.sessionPage', sessionInfo.sessionPage); } if (sessionInfo.sessionNum > 0) { deepSetValue(request, 'user.ext.mgid.sessionNum', sessionInfo.sessionNum); if (sessionInfo.sessionsWeek > 0) { deepSetValue(request, 'user.ext.mgid.sessions1w', sessionInfo.sessionsWeek); } if (isNumber(sessionInfo.timeBetweenSessions)) { deepSetValue(request, 'user.ext.mgid.timeBetweenSessions', sessionInfo.timeBetweenSessions); } } } function populateConnectionInfo(request) { const conn = getConnectionInfo(); if (conn) { if (isStr(conn.effectiveType)) { deepSetValue(request, 'site.ext.mgid.niet', conn.effectiveType); } if (typeof conn.saveData === 'boolean') { deepSetValue(request, 'site.ext.mgid.nisd', conn.saveData ? 1 : 0); } } } /** * Publisher opt-in for MGID usage data: `pbjs.bidderSettings.mgid = {enhancedBidData: true}`. * Disabled by default. * @returns {boolean} */ function isEnhancedBidDataEnabled() { return bidderSettings.get(BIDDER_CODE, 'enhancedBidData') === true; } /** * @returns {boolean} */ function isViewabilityEnabled() { const installedModules = getGlobal().installedModules; return (installedModules.includes('bidViewability') && config.getConfig('bidViewability')?.enabled === true) || (installedModules.includes('bidViewabilityIO') && config.getConfig('bidViewabilityIO')?.enabled === true); } /** * @param {MgidBidRequest} bidRequest * @returns {{ id: string, type: string }} */ function resolveStorageId(bidRequest) { const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid'); if (isStr(gpid) && gpid.length > 0) { return { id: gpid, type: 'gpid' }; } const pbadslot = deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); if (isStr(pbadslot) && pbadslot.length > 0) { return { id: pbadslot, type: 'pbadslot' }; } const tagId = bidRequest && bidRequest.adUnitCode; if (isStr(tagId) && tagId.length > 0) { return { id: tagId, type: 'tagId' }; } return { id: '', type: '' }; } function getLocalStorageSafely(key) { try { return storage.getDataFromLocalStorage(key); } catch (e) { return null; } } function setLocalStorageSafely(key, val) { try { return storage.setDataInLocalStorage(key, val); } catch (e) { return null; } } function getBidFloor(bid, reqCur) { let bidFloor = getBidIdParameter('bidFloor', bid.params) || getBidIdParameter('bidfloor', bid.params) || 0; let cur = ''; if (!bidFloor && isFn(bid.getFloor)) { const floorObj = bid.getFloor({ currency: '*', mediaType: '*', size: '*' }); if (isPlainObject(floorObj) && !isNaN(floorObj.floor)) { bidFloor = floorObj.floor; const floorCur = isStr(floorObj.currency) ? floorObj.currency : DEFAULT_CUR; if (floorCur !== reqCur) { cur = floorCur; } } } return { floor: bidFloor, cur: cur }; }