diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 06f96edca30..652574402a8 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -635,7 +635,8 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r = applyRegulations(r, bidderRequest); let payload = {}; - createPayload(validBidRequests, bidderRequest, r, baseUrl, requests, payload); + siteID = validBidRequests[0].params.siteId; + payload.s = siteID; const transactionIds = Object.keys(impressions); let isFpdAdded = false; @@ -649,15 +650,13 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { const fpd = deepAccess(bidderRequest, 'ortb2') || {}; const site = { ...(fpd.site || fpd.context) }; + + // update page URL with IX FPD KVs if they exist + site.page = getIxFirstPartyDataPageUrl(bidderRequest); + const user = { ...fpd.user }; if (!isEmpty(fpd) && !isFpdAdded) { r = addFPD(bidderRequest, r, fpd, site, user); - - const clonedRObject = deepClone(r); - - clonedRObject.site = mergeDeep({}, clonedRObject.site, site); - clonedRObject.user = mergeDeep({}, clonedRObject.user, user); - r.site = mergeDeep({}, r.site, site); r.user = mergeDeep({}, r.user, user); isFpdAdded = true; @@ -856,46 +855,6 @@ function applyRegulations(r, bidderRequest) { return r } -/** - * createPayload creates the payload to be sent with the request. - * - * @param {array} validBidRequests A list of valid bid request config objects. - * @param {object} bidderRequest An object containing other info like gdprConsent. - * @param {object} r Reuqest object. - * @param {string} baseUrl Base exchagne URL. - * @param {array} requests List of request obejcts. - * @param {object} payload Request payload object. - */ -function createPayload(validBidRequests, bidderRequest, r, baseUrl, requests, payload) { - // Use the siteId in the first bid request as the main siteId. - siteID = validBidRequests[0].params.siteId; - payload.s = siteID; - - // Parse additional runtime configs. - const bidderCode = (bidderRequest && bidderRequest.bidderCode) || 'ix'; - const otherIxConfig = config.getConfig(bidderCode); - - if (otherIxConfig) { - // Append firstPartyData to r.site.page if firstPartyData exists. - if (typeof otherIxConfig.firstPartyData === 'object') { - const firstPartyData = otherIxConfig.firstPartyData; - let firstPartyString = '?'; - for (const key in firstPartyData) { - if (firstPartyData.hasOwnProperty(key)) { - firstPartyString += `${encodeURIComponent(key)}=${encodeURIComponent(firstPartyData[key])}&`; - } - } - firstPartyString = firstPartyString.slice(0, -1); - - if ('page' in r.site) { - r.site.page += firstPartyString; - } else { - r.site.page = firstPartyString; - } - } - } -} - /** * addImpressions adds impressions to request object * @@ -977,6 +936,65 @@ function addImpressions(impressions, transactionIds, r, adUnitIndex) { return r; } +/** +This function retrieves the page URL and appends first party data query parameters +to it without adding duplicate query parameters. Returns original referer URL if no IX FPD exists. +@param {Object} bidderRequest - The bidder request object containing information about the bid and the page. +@returns {string} - The modified page URL with first party data query parameters appended. +*/ +function getIxFirstPartyDataPageUrl (bidderRequest) { + // Parse additional runtime configs. + const bidderCode = (bidderRequest && bidderRequest.bidderCode) || 'ix'; + const otherIxConfig = config.getConfig(bidderCode); + + let pageUrl = ''; + if (deepAccess(bidderRequest, 'ortb2.site.page')) { + pageUrl = bidderRequest.ortb2.site.page; + } else { + pageUrl = deepAccess(bidderRequest, 'refererInfo.page'); + } + + if (otherIxConfig) { + // Append firstPartyData to r.site.page if firstPartyData exists. + if (typeof otherIxConfig.firstPartyData === 'object') { + const firstPartyData = otherIxConfig.firstPartyData; + return appendIXQueryParams(bidderRequest, pageUrl, firstPartyData); + } + } + + return pageUrl +} + +/** +This function appends the provided query parameters to the given URL without adding duplicate query parameters. +@param {Object} bidderRequest - The bidder request object containing information about the bid and the page to be used as fallback in case url is not valid. +@param {string} url - The base URL to which query parameters will be appended. +@param {Object} params - An object containing key-value pairs of query parameters to append. +@returns {string} - The modified URL with the provided query parameters appended. +*/ +function appendIXQueryParams(bidderRequest, url, params) { + let urlObj; + try { + urlObj = new URL(url); + } catch (error) { + logWarn(`IX Bid Adapter: Invalid URL set in ortb2.site.page: ${url}. Using referer URL instead.`); + urlObj = new URL(deepAccess(bidderRequest, 'refererInfo.page')); + } + + const searchParams = new URLSearchParams(urlObj.search); + + // Loop through the provided query parameters and append them + for (const [key, value] of Object.entries(params)) { + if (!searchParams.has(key)) { + searchParams.append(key, value); + } + } + + // Construct the final URL with the updated query parameters + urlObj.search = searchParams.toString(); + return urlObj.toString(); +} + /** * addFPD adds ortb2 first party data to request object. * diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 149c4c44c21..874f5048ce0 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -725,6 +725,11 @@ describe('IndexexchangeAdapter', function () { refererInfo: { page: 'https://www.prebid.org', canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' + }, + ortb2: { + site: { + page: 'https://www.prebid.org' + } } }; @@ -2135,7 +2140,7 @@ describe('IndexexchangeAdapter', function () { const requestWithFirstPartyData = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION)[0]; const pageUrl = extractPayload(requestWithFirstPartyData).site.page; - const expectedPageUrl = DEFAULT_OPTION.refererInfo.page + '?ab=123&cd=123%23ab&e%2Ff=456&h%3Fg=456%23cd'; + const expectedPageUrl = DEFAULT_OPTION.ortb2.site.page + '/?ab=123&cd=123%23ab&e%2Ff=456&h%3Fg=456%23cd'; expect(pageUrl).to.equal(expectedPageUrl); }); @@ -2233,6 +2238,66 @@ describe('IndexexchangeAdapter', function () { }); }); + describe('getIxFirstPartyDataPageUrl', () => { + beforeEach(() => { + config.setConfig({ ix: { firstPartyData: { key1: 'value1', key2: 'value2' } } }); + }); + + afterEach(() => { + config.resetConfig(); + }); + + it('should return the modified URL with first party data query parameters appended', () => { + const requestWithIXFirstPartyData = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION)[0]; + const pageUrl = extractPayload(requestWithIXFirstPartyData).site.page; + expect(pageUrl).to.equal('https://www.prebid.org/?key1=value1&key2=value2'); + }); + + it('should return the modified URL with first party data query parameters appended but not duplicated', () => { + const bidderRequest = deepClone(DEFAULT_OPTION); + bidderRequest.ortb2.site.page = 'https://www.prebid.org/?key1=value1' + const requestWithIXFirstPartyData = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION)[0]; + const pageUrl = extractPayload(requestWithIXFirstPartyData).site.page; + expect(pageUrl).to.equal('https://www.prebid.org/?key1=value1&key2=value2'); + }); + + it('should not overwrite existing query parameters with first party data', () => { + config.setConfig({ ix: { firstPartyData: { key1: 'value1', key2: 'value2' } } }); + const bidderRequest = deepClone(DEFAULT_OPTION); + bidderRequest.ortb2.site.page = 'https://www.prebid.org/?key1=existingValue1' + const requestWithIXFirstPartyData = spec.buildRequests(DEFAULT_BANNER_VALID_BID, bidderRequest)[0]; + const pageUrl = extractPayload(requestWithIXFirstPartyData).site.page; + expect(pageUrl).to.equal('https://www.prebid.org/?key1=existingValue1&key2=value2'); + }); + + it('should return the original URL if no first party data is available', () => { + config.setConfig({ ix: {} }); + const requestWithIXFirstPartyData = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION)[0]; + const pageUrl = extractPayload(requestWithIXFirstPartyData).site.page; + expect(pageUrl).to.equal('https://www.prebid.org'); + }); + + it('should return the original URL referer page url if ortb2 does not exist', () => { + config.setConfig({ ix: {} }); + const bidderRequest = deepClone(DEFAULT_OPTION); + delete bidderRequest.ortb2; + bidderRequest.refererInfo.page = 'https://example.com'; + const requestWithIXFirstPartyData = spec.buildRequests(DEFAULT_BANNER_VALID_BID, bidderRequest)[0]; + const pageUrl = extractPayload(requestWithIXFirstPartyData).site.page; + expect(pageUrl).to.equal('https://example.com'); + }); + + it('should use referer URL if the provided ortb2.site.page URL is not valid', () => { + config.setConfig({ ix: { firstPartyData: { key1: 'value1', key2: 'value2' } } }); + const bidderRequest = deepClone(DEFAULT_OPTION); + bidderRequest.ortb2.site.page = 'www.invalid-url*&?.com'; + bidderRequest.refererInfo.page = 'https://www.prebid.org'; + const requestWithIXFirstPartyData = spec.buildRequests(DEFAULT_BANNER_VALID_BID, bidderRequest)[0]; + const pageUrl = extractPayload(requestWithIXFirstPartyData).site.page; + expect(pageUrl).to.equal('https://www.prebid.org/?key1=value1&key2=value2'); + }); + }); + describe('request should contain both banner and video requests', function () { const request = spec.buildRequests([DEFAULT_BANNER_VALID_BID[0], DEFAULT_VIDEO_VALID_BID[0]], {}); it('should have banner request', () => {