diff --git a/.github/workflows/issue_tracker.yml b/.github/workflows/issue_tracker.yml index fa33ffe5c53..35c6db80c26 100644 --- a/.github/workflows/issue_tracker.yml +++ b/.github/workflows/issue_tracker.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Generate token id: generate_token - uses: tibdex/github-app-token@36464acb844fc53b9b8b2401da68844f6b05ebb0 + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 with: app_id: ${{ secrets.ISSUE_APP_ID }} private_key: ${{ secrets.ISSUE_APP_PEM }} diff --git a/modules/luponmediaBidAdapter.js b/modules/luponmediaBidAdapter.js index 31e2120d364..f0294cd43e5 100755 --- a/modules/luponmediaBidAdapter.js +++ b/modules/luponmediaBidAdapter.js @@ -1,4 +1,4 @@ -import {isArray, logMessage, deepAccess, logWarn, parseSizesInput, deepSetValue, generateUUID, isEmpty, logError, _each, isFn} from '../src/utils.js'; +import {isArray, deepAccess, logWarn, parseSizesInput, deepSetValue, generateUUID, mergeDeep, logError, _each, isFn, formatQS} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER} from '../src/mediaTypes.js'; @@ -7,13 +7,6 @@ import { ajax } from '../src/ajax.js'; const BIDDER_CODE = 'luponmedia'; const ENDPOINT_URL = 'https://rtb.adxpremium.services/openrtb2/auction'; -const DIGITRUST_PROP_NAMES = { - PREBID_SERVER: { - id: 'id', - keyv: 'keyv' - } -}; - var sizeMap = { 1: '468x60', 2: '728x90', @@ -170,44 +163,32 @@ export const spec = { } return bidResponses; }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { - let allUserSyncs = []; - if (!hasSynced && (syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { - responses.forEach(csResp => { - if (csResp.body && csResp.body.ext && csResp.body.ext.usersyncs) { - try { - let response = csResp.body.ext.usersyncs - let bidders = response.bidder_status; - for (let synci in bidders) { - let thisSync = bidders[synci]; - if (thisSync.no_cookie) { - let url = thisSync.usersync.url; - let type = thisSync.usersync.type; - - if (!url) { - logError(`No sync url for bidder luponmedia.`); - } else if ((type === 'image' || type === 'redirect') && syncOptions.pixelEnabled) { - logMessage(`Invoking image pixel user sync for luponmedia`); - allUserSyncs.push({type: 'image', url: url}); - } else if (type == 'iframe' && syncOptions.iframeEnabled) { - logMessage(`Invoking iframe user sync for luponmedia`); - allUserSyncs.push({type: 'iframe', url: url}); - } else { - logError(`User sync type "${type}" not supported for luponmedia`); - } - } - } - } catch (e) { - logError(e); - } + getUserSyncs: function (syncOptions, _responses, gdprConsent, uspConsent) { + if (!hasSynced && syncOptions.iframeEnabled) { + // data is only assigned if params are available to pass to syncEndpoint + let params = {}; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params['gdpr'] = Number(gdprConsent.gdprApplies); } - }); - } else { - logWarn('Luponmedia: Please enable iframe/pixel based user sync.'); - } + if (typeof gdprConsent.consentString === 'string') { + params['gdpr_consent'] = gdprConsent.consentString; + } + } + + if (uspConsent) { + params['us_privacy'] = encodeURIComponent(uspConsent); + } + + params = Object.keys(params).length ? `?${formatQS(params)}` : ''; - hasSynced = true; - return allUserSyncs; + hasSynced = true; + return { + type: 'iframe', + url: `https://user-sync.adxpremium.services/load-cookie.html` + params + }; + } }, onBidWon: bid => { const bidString = JSON.stringify(bid); @@ -316,33 +297,27 @@ function newOrtbBidRequest(bidRequest, bidderRequest, currentImps) { } } - let bidFloor; - if (isFn(bidRequest.getFloor) && !config.getConfig('disableFloors')) { - let floorInfo; - try { - floorInfo = bidRequest.getFloor({ - currency: 'USD', - mediaType: 'video', - size: parseSizes(bidRequest, 'video') - }); - } catch (e) { - logError('LuponMedia: getFloor threw an error: ', e); - } - bidFloor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? parseFloat(floorInfo.floor) : undefined; - } else { - bidFloor = parseFloat(deepAccess(bidRequest, 'params.floor')); - } - if (!isNaN(bidFloor)) { - data.imp[0].bidfloor = bidFloor; - } + setBidFloor(bidRequest, data); appendSiteAppDevice(data, bidRequest, bidderRequest); - const digiTrust = _getDigiTrustQueryParams(bidRequest, 'PREBID_SERVER'); - if (digiTrust) { - deepSetValue(data, 'user.ext.digitrust', digiTrust); + setGdprAndPrivacy(bidderRequest, data); + setUserId(bidRequest, data); + + if (config.getConfig('coppa') === true) { + deepSetValue(data, 'regs.coppa', 1); + } + + if (bidRequest.schain && hasValidSupplyChainParams(bidRequest.schain)) { + deepSetValue(data, 'source.ext.schain', bidRequest.schain); } + setSiteAndUserData(bidRequest, BANNER, data); + + return data; +} + +function setGdprAndPrivacy(bidderRequest, data) { if (bidderRequest.gdprConsent) { // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module let gdprApplies; @@ -357,7 +332,9 @@ function newOrtbBidRequest(bidRequest, bidderRequest, currentImps) { if (bidderRequest.uspConsent) { deepSetValue(data, 'regs.ext.us_privacy', bidderRequest.uspConsent); } +} +function setUserId(bidRequest, data) { // Set user uuid deepSetValue(data, 'user.id', generateUUID()); @@ -369,134 +346,91 @@ function newOrtbBidRequest(bidRequest, bidderRequest, currentImps) { } if (bidRequest.userId && typeof bidRequest.userId === 'object' && - (bidRequest.userId.tdid || bidRequest.userId.pubcid || bidRequest.userId.lipb || bidRequest.userId.idl_env)) { + (bidRequest.userId.tdid || bidRequest.userId.pubcid || bidRequest.userId.lipb || bidRequest.userId.idl_env)) { deepSetValue(data, 'user.ext.eids', []); - - if (bidRequest.userId.tdid) { - data.user.ext.eids.push({ - source: 'adserver.org', - uids: [{ - id: bidRequest.userId.tdid, - ext: { - rtiPartner: 'TDID' - } - }] - }); - } - - if (bidRequest.userId.pubcid) { - data.user.ext.eids.push({ - source: 'pubcommon', - uids: [{ - id: bidRequest.userId.pubcid, - }] - }); - } - - // support liveintent ID - if (bidRequest.userId.lipb && bidRequest.userId.lipb.lipbid) { - data.user.ext.eids.push({ - source: 'liveintent.com', - uids: [{ - id: bidRequest.userId.lipb.lipbid - }] - }); - - data.user.ext.tpid = { - source: 'liveintent.com', - uid: bidRequest.userId.lipb.lipbid - }; - - if (Array.isArray(bidRequest.userId.lipb.segments) && bidRequest.userId.lipb.segments.length) { - deepSetValue(data, 'rp.target.LIseg', bidRequest.userId.lipb.segments); - } - } - - // support identityLink (aka LiveRamp) - if (bidRequest.userId.idl_env) { - data.user.ext.eids.push({ - source: 'liveramp.com', - uids: [{ - id: bidRequest.userId.idl_env - }] - }); - } + setAdserverOrg(bidRequest, data); + setPubcommon(bidRequest, data); + setLiveIntent(bidRequest, data); + setIdentityLink(bidRequest, data); } +} - if (config.getConfig('coppa') === true) { - deepSetValue(data, 'regs.coppa', 1); +function setAdserverOrg(bidRequest, data) { + if (bidRequest.userId.tdid) { + data.user.ext.eids.push({ + source: 'adserver.org', + uids: [{ + id: bidRequest.userId.tdid, + ext: { + rtiPartner: 'TDID' + } + }] + }); } +} - if (bidRequest.schain && hasValidSupplyChainParams(bidRequest.schain)) { - deepSetValue(data, 'source.ext.schain', bidRequest.schain); - } +function setLiveIntent(bidRequest, data) { + if (bidRequest.userId.lipb && bidRequest.userId.lipb.lipbid) { + data.user.ext.eids.push({ + source: 'liveintent.com', + uids: [{ + id: bidRequest.userId.lipb.lipbid + }] + }); - // TODO: getConfig('fpd.context') should not have worked even with legacy FPD support - 'fpd' gets translated - // into 'ortb2' by `setConfig` - // Unclear what the intent was here - maybe `const {context: siteData, user: userData} = getLegacyFpd(config.getConfig('ortb2'))` ? - // (with PB7 `config.getConfig('ortb2')` should be replaced by `bidderRequest.ortb2`) - const siteData = Object.assign({}, bidRequest.params.inventory, config.getConfig('fpd.context')); - const userData = Object.assign({}, bidRequest.params.visitor, config.getConfig('fpd.user')); - - if (!isEmpty(siteData) || !isEmpty(userData)) { - const bidderData = { - bidders: [ bidderRequest.bidderCode ], - config: { - fpd: {} - } + data.user.ext.tpid = { + source: 'liveintent.com', + uid: bidRequest.userId.lipb.lipbid }; - if (!isEmpty(siteData)) { - bidderData.config.fpd.site = siteData; - } - - if (!isEmpty(userData)) { - bidderData.config.fpd.user = userData; + if (Array.isArray(bidRequest.userId.lipb.segments) && bidRequest.userId.lipb.segments.length) { + deepSetValue(data, 'rp.target.LIseg', bidRequest.userId.lipb.segments); } - - deepSetValue(data, 'ext.prebid.bidderconfig.0', bidderData); } +} - // TODO: bidRequest.fpd is not the right place for pbadslot - who's filling that in, if anyone? - // is this meant to be bidRequest.ortb2Imp.ext.data.pbadslot? - const pbAdSlot = deepAccess(bidRequest, 'fpd.context.pbAdSlot'); - if (typeof pbAdSlot === 'string' && pbAdSlot) { - deepSetValue(data.imp[0].ext, 'context.data.adslot', pbAdSlot); +function setIdentityLink(bidRequest, data) { + if (bidRequest.userId.idl_env) { + data.user.ext.eids.push({ + source: 'liveramp.com', + uids: [{ + id: bidRequest.userId.idl_env + }] + }); } - - return data; } -function _getDigiTrustQueryParams(bidRequest = {}, endpointName) { - if (!endpointName || !DIGITRUST_PROP_NAMES[endpointName]) { - return null; +function setPubcommon(bidRequest, data) { + if (bidRequest.userId.pubcid) { + data.user.ext.eids.push({ + source: 'pubcommon', + uids: [{ + id: bidRequest.userId.pubcid, + }] + }); } - const propNames = DIGITRUST_PROP_NAMES[endpointName]; +} - function getDigiTrustId() { - const bidRequestDigitrust = deepAccess(bidRequest, 'userId.digitrustid.data'); - if (bidRequestDigitrust) { - return bidRequestDigitrust; +function setBidFloor(bidRequest, data) { + let bidFloor; + if (isFn(bidRequest.getFloor) && !config.getConfig('disableFloors')) { + let floorInfo; + try { + floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'video', + size: parseSizes(bidRequest, 'video') + }); + } catch (e) { + logError('LuponMedia: getFloor threw an error: ', e); } - - let digiTrustUser = (window.DigiTrust && (config.getConfig('digiTrustId') || window.DigiTrust.getUser({member: 'T9QSFKPDN9'}))); - return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; - } - - let digiTrustId = getDigiTrustId(); - // Verify there is an ID and this user has not opted out - if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) { - return null; + bidFloor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? parseFloat(floorInfo.floor) : undefined; + } else { + bidFloor = parseFloat(deepAccess(bidRequest, 'params.floor')); } - - const digiTrustQueryParams = { - [propNames.id]: digiTrustId.id, - [propNames.keyv]: digiTrustId.keyv - }; - if (propNames.pref) { - digiTrustQueryParams[propNames.pref] = 0; + if (!isNaN(bidFloor)) { + data.imp[0].bidfloor = bidFloor; } - return digiTrustQueryParams; } function _getPageUrl(bidRequest, bidderRequest) { @@ -574,4 +508,88 @@ function parseSizes(bid, mediaType) { return masSizeOrdering(sizes); } +function setSiteAndUserData(bidRequest, mediaType, data) { + const BID_FPD = { + user: {ext: {data: {...bidRequest.params.visitor}}}, + site: {ext: {data: {...bidRequest.params.inventory}}} + }; + + if (bidRequest.params.keywords) BID_FPD.site.keywords = (isArray(bidRequest.params.keywords)) ? bidRequest.params.keywords.join(',') : bidRequest.params.keywords; + + let fpd = mergeDeep({}, bidRequest.ortb2 || {}, BID_FPD); + let impData = deepAccess(bidRequest.ortb2Imp, 'ext.data') || {}; + + const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid'); + const SEGTAX = {user: [4], site: [1, 2, 5, 6]}; + const MAP = {user: 'tg_v.', site: 'tg_i.', adserver: 'tg_i.dfp_ad_unit_code', pbadslot: 'tg_i.pbadslot', keywords: 'kw'}; + const validate = function(prop, key, parentName) { + if (key === 'data' && Array.isArray(prop)) { + return prop.filter(name => name.segment && deepAccess(name, 'ext.segtax') && SEGTAX[parentName] && + SEGTAX[parentName].indexOf(deepAccess(name, 'ext.segtax')) !== -1).map(value => { + let segments = value.segment.filter(obj => obj.id).reduce((result, obj) => { + result.push(obj.id); + return result; + }, []); + if (segments.length > 0) return segments.toString(); + }).toString(); + } else if (typeof prop === 'object' && !Array.isArray(prop)) { + logWarn('LuponMedia: Filtered FPD key: ', key, ': Expected value to be string, integer, or an array of strings/ints'); + } else if (typeof prop !== 'undefined') { + return (Array.isArray(prop)) ? prop.filter(value => { + if (typeof value !== 'object' && typeof value !== 'undefined') return value.toString(); + + logWarn('LuponMedia: Filtered value: ', value, 'for key', key, ': Expected value to be string, integer, or an array of strings/ints'); + }).toString() : prop.toString(); + } + }; + const addBannerData = function(obj, name, key, isParent = true) { + let val = validate(obj, key, name); + let loc = (MAP[key] && isParent) ? `${MAP[key]}` : (key === 'data') ? `${MAP[name]}iab` : `${MAP[name]}${key}`; + data[loc] = (data[loc]) ? data[loc].concat(',', val) : val; + } + + if (mediaType === BANNER) { + ['site', 'user'].forEach(name => { + Object.keys(fpd[name]).forEach((key) => { + if (name === 'site' && key === 'content' && fpd[name][key].data) { + addBannerData(fpd[name][key].data, name, 'data'); + } else if (key !== 'ext') { + addBannerData(fpd[name][key], name, key); + } else if (fpd[name][key].data) { + Object.keys(fpd[name].ext.data).forEach((key) => { + addBannerData(fpd[name].ext.data[key], name, key, false); + }); + } + }); + }); + Object.keys(impData).forEach((key) => { + if (key !== 'adserver') { + addBannerData(impData[key], 'site', key); + } else if (impData[key].name === 'gam') { + addBannerData(impData[key].adslot, name, key) + } + }); + + // add in gpid + if (gpid) { + data['p_gpid'] = gpid; + } + + // only send one of pbadslot or dfp adunit code (prefer pbadslot) + if (data['tg_i.pbadslot']) { + delete data['tg_i.dfp_ad_unit_code']; + } + } else { + if (Object.keys(impData).length) { + mergeDeep(data.imp[0].ext, {data: impData}); + } + // add in gpid + if (gpid) { + data.imp[0].ext.gpid = gpid; + } + + mergeDeep(data, fpd); + } +} + registerBidder(spec); diff --git a/test/spec/modules/luponmediaBidAdapter_spec.js b/test/spec/modules/luponmediaBidAdapter_spec.js index 9f109ff1892..5c269ca7639 100755 --- a/test/spec/modules/luponmediaBidAdapter_spec.js +++ b/test/spec/modules/luponmediaBidAdapter_spec.js @@ -234,100 +234,121 @@ describe('luponmediaBidAdapter', function () { }); }); - describe('getUserSyncs', function () { - const bidResponse1 = { - 'body': { - 'ext': { - 'responsetimemillis': { - 'luponmedia': 233 - }, - 'tmaxrequest': 1500, - 'usersyncs': { - 'status': 'ok', - 'bidder_status': [ - { - 'bidder': 'luponmedia', - 'no_cookie': true, - 'usersync': { - 'url': 'https://adxpremium.services/api/usersync', - 'type': 'redirect' - } - }, - { - 'bidder': 'luponmedia', - 'no_cookie': true, - 'usersync': { - 'url': 'https://adxpremium.services/api/iframeusersync', - 'type': 'iframe' - } - } - ] - } - } - } - }; + describe('get user sync', function () { + const syncUrl = 'https://user-sync.adxpremium.services/load-cookie.html'; - const bidResponse2 = { - 'body': { - 'ext': { - 'responsetimemillis': { - 'luponmedia': 233 - }, - 'tmaxrequest': 1500, - 'usersyncs': { - 'status': 'no_cookie', - 'bidder_status': [] - } - } - } - }; + beforeEach(function () { + resetUserSync(); + }); - it('should use a sync url from first response (pixel and iframe)', function () { - const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [bidResponse1, bidResponse2]); - expect(syncs).to.deep.equal([ - { - type: 'image', - url: 'https://adxpremium.services/api/usersync' - }, - { - type: 'iframe', - url: 'https://adxpremium.services/api/iframeusersync' - } - ]); + it('should register the LuponMedia iframe', function () { + let syncs = spec.getUserSyncs({ + iframeEnabled: true + }); + + expect(syncs).to.deep.equal({type: 'iframe', url: syncUrl}); }); - it('handle empty response (e.g. timeout)', function () { - const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, []); - expect(syncs).to.deep.equal([]); + it('should not register the LuponMedia iframe more than once', function () { + let syncs = spec.getUserSyncs({ + iframeEnabled: true + }); + expect(syncs).to.deep.equal({type: 'iframe', url: syncUrl}); + + // when called again, should still have only been called once + syncs = spec.getUserSyncs(); + expect(syncs).to.equal(undefined); }); - it('returns empty syncs when not pixel enabled and not iframe enabled', function () { - const syncs = spec.getUserSyncs({ pixelEnabled: false, iframeEnabled: false }, [bidResponse1]); - expect(syncs).to.deep.equal([]); + it('should pass gdpr params if consent is true', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { + gdprApplies: true, consentString: 'foo' + })).to.deep.equal({ + type: 'iframe', url: `${syncUrl}?gdpr=1&gdpr_consent=foo` + }); }); - it('returns pixel syncs when pixel enabled and not iframe enabled', function() { - resetUserSync(); + it('should pass gdpr params if consent is false', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { + gdprApplies: false, consentString: 'foo' + })).to.deep.equal({ + type: 'iframe', url: `${syncUrl}?gdpr=0&gdpr_consent=foo` + }); + }); - const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: false }, [bidResponse1]); - expect(syncs).to.deep.equal([ - { - type: 'image', - url: 'https://adxpremium.services/api/usersync' - } - ]); + it('should pass gdpr param gdpr_consent only when gdprApplies is undefined', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { + consentString: 'foo' + })).to.deep.equal({ + type: 'iframe', url: `${syncUrl}?gdpr_consent=foo` + }); }); - it('returns iframe syncs when not pixel enabled and iframe enabled', function() { - resetUserSync(); + it('should pass no params if gdpr consentString is not defined', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, {})).to.deep.equal({ + type: 'iframe', url: `${syncUrl}` + }); + }); - const syncs = spec.getUserSyncs({ pixelEnabled: false, iframeEnabled: true }, [bidResponse1]); - expect(syncs).to.deep.equal([ - { - type: 'iframe', - url: 'https://adxpremium.services/api/iframeusersync' - } - ]); + it('should pass no params if gdpr consentString is a number', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { + consentString: 0 + })).to.deep.equal({ + type: 'iframe', url: `${syncUrl}` + }); + }); + + it('should pass no params if gdpr consentString is null', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { + consentString: null + })).to.deep.equal({ + type: 'iframe', url: `${syncUrl}` + }); + }); + + it('should pass no params if gdpr consentString is a object', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { + consentString: {} + })).to.deep.equal({ + type: 'iframe', url: `${syncUrl}` + }); + }); + + it('should pass no params if gdpr is not defined', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, undefined)).to.deep.equal({ + type: 'iframe', url: `${syncUrl}` + }); + }); + + it('should pass us_privacy if uspConsent is defined', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, undefined, '1NYN')).to.deep.equal({ + type: 'iframe', url: `${syncUrl}?us_privacy=1NYN` + }); + }); + + it('should pass us_privacy after gdpr if both are present', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { + consentString: 'foo' + }, '1NYN')).to.deep.equal({ + type: 'iframe', url: `${syncUrl}?gdpr_consent=foo&us_privacy=1NYN` + }); + }); + + it('should pass gdprApplies', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { + gdprApplies: true + }, '1NYN')).to.deep.equal({ + type: 'iframe', url: `${syncUrl}?gdpr=1&us_privacy=1NYN` + }); + }); + + it('should pass all correctly', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { + gdprApplies: true, + consentString: 'foo' + }, '1NYN')).to.deep.equal({ + type: 'iframe', url: `${syncUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` + }); }); });