From e7d3b8a7adccb5433c61766d1ab658c1dce1fa1a Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Fri, 7 Jan 2022 12:16:33 +0200 Subject: [PATCH 01/23] AdHash Bidder Adapter: minor changes We're operating on a com TLD now. Added publisher in URL for easier routing. --- modules/adhashBidAdapter.js | 6 +++--- test/spec/modules/adhashBidAdapter_spec.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index c94a4e35efd..6a8c98650c0 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -6,7 +6,7 @@ const VERSION = '1.0'; export const spec = { code: 'adhash', - url: 'https://bidder.adhash.org/rtb?version=' + VERSION + '&prebid=true', + url: 'https://bidder.adhash.com/rtb?version=' + VERSION + '&prebid=true', supportedMediaTypes: [ BANNER ], isBidRequestValid: (bid) => { @@ -37,7 +37,7 @@ export const spec = { var size = validBidRequests[i].sizes[index].join('x'); bidRequests.push({ method: 'POST', - url: url, + url: url + '&publisher=' + validBidRequests[i].params.publisherId, bidRequest: validBidRequests[i], data: { timezone: new Date().getTimezoneOffset() / 60, @@ -87,7 +87,7 @@ export const spec = { cpm: responseBody.creatives[0].costEUR, ad: `
- + `, width: request.bidRequest.sizes[0][0], height: request.bidRequest.sizes[0][1], diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js index eda164da852..6c214b84928 100644 --- a/test/spec/modules/adhashBidAdapter_spec.js +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -77,7 +77,7 @@ describe('adhashBidAdapter', function () { ); expect(result.length).to.equal(1); expect(result[0].method).to.equal('POST'); - expect(result[0].url).to.equal('https://bidder.adhash.org/rtb?version=1.0&prebid=true'); + expect(result[0].url).to.equal('https://bidder.adhash.com/rtb?version=1.0&prebid=true&publisher=0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb'); expect(result[0].bidRequest).to.equal(bidRequest); expect(result[0].data).to.have.property('timezone'); expect(result[0].data).to.have.property('location'); @@ -93,7 +93,7 @@ describe('adhashBidAdapter', function () { const result = spec.buildRequests([ bidRequest ], { gdprConsent: true }); expect(result.length).to.equal(1); expect(result[0].method).to.equal('POST'); - expect(result[0].url).to.equal('https://bidder.adhash.org/rtb?version=1.0&prebid=true'); + expect(result[0].url).to.equal('https://bidder.adhash.com/rtb?version=1.0&prebid=true&publisher=0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb'); expect(result[0].bidRequest).to.equal(bidRequest); expect(result[0].data).to.have.property('timezone'); expect(result[0].data).to.have.property('location'); From 814f3a80c8dc68723d990c2178e13bb61c75b7bd Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Tue, 18 Jan 2022 13:50:38 +0200 Subject: [PATCH 02/23] Implemented brand safety Implemented brand safety checks --- modules/adhashBidAdapter.js | 75 +++++++++++++++++++++- test/spec/modules/adhashBidAdapter_spec.js | 72 ++++++++++++++++++--- 2 files changed, 137 insertions(+), 10 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 6a8c98650c0..300f2e183e6 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -3,6 +3,75 @@ import includes from 'core-js-pure/features/array/includes.js'; import { BANNER } from '../src/mediaTypes.js'; const VERSION = '1.0'; +const BAD_WORD_STEP = 0.1; +const BAD_WORD_MIN = 0.2; + +/** + * Function that checks the page where the ads are being served for brand safety. + * If unsafe words are found the scoring of that page increases. + * If it becomes greater than the maximum allowed score false is returned. + * The rules may vary based on the website language or the publisher. + * The AdHash bidder will not bid on unsafe pages (according to 4A's). + * @param badWords list of scoring rules to chech against + * @param maxScore maximum allowed score for that bidding + * @returns boolean flag is the page safe + */ +function brandSafety(badWords, maxScore) { + /** + * Performs the ROT13 encoding on the string argument and returns the resulting string. + * The Adhash bidder uses ROT13 so that the response is not blocked by: + * - ad blocking software + * - parental control software + * - corporate firewalls + * due to the bad words contained in the response. + * @param value The input string. + * @returns string Returns the ROT13 version of the given string. + */ + const rot13 = value => { + const input = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + const output = 'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm'; + const index = x => input.indexOf(x); + const translate = x => index(x) > -1 ? output[index(x)] : x; + return value.split('').map(translate).join(''); + }; + + /** + * Calculates the scoring for each bad word with dimishing returns + * @param {integer} points points that this word costs + * @param {integer} occurances number of occurances + * @returns {float} final score + */ + const scoreCalculator = (points, occurances) => { + let result = 0; + for (let i = 0; i < occurances; i++) { + result += Math.max(points - i * BAD_WORD_STEP, BAD_WORD_MIN); + } + return result; + }; + + // Default parameters if the bidder is unable to send some of them + badWords = badWords || []; + maxScore = parseInt(maxScore) || 10; + + try { + let score = 0; + const content = window.top.document.body.innerText.toLowerCase(); + const words = content.trim().split(/\s+/).length; + const factor = words < 500 ? 1 : words < 2000 ? 2 : 4; + for (const [word, rule, points] of badWords) { + if (rule === 'full' && new RegExp('\\b' + rot13(word) + '\\b', 'i').test(content)) { + const occurances = content.match(new RegExp('\\b' + rot13(word) + '\\b', 'g')).length; + score += scoreCalculator(points, occurances); + } else if (rule === 'partial' && content.indexOf(rot13(word.toLowerCase())) > -1) { + const occurances = content.match(new RegExp(rot13(word), 'g')).length; + score += scoreCalculator(points, occurances); + } + } + return score < maxScore * factor; + } catch (e) { + return true; + } +} export const spec = { code: 'adhash', @@ -73,7 +142,11 @@ export const spec = { interpretResponse: (serverResponse, request) => { const responseBody = serverResponse ? serverResponse.body : {}; - if (!responseBody.creatives || responseBody.creatives.length === 0) { + if ( + !responseBody.creatives || + responseBody.creatives.length === 0 || + !brandSafety(responseBody.badWords, responseBody.maxScore) + ) { return []; } diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js index 6c214b84928..b30c32715c3 100644 --- a/test/spec/modules/adhashBidAdapter_spec.js +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -7,7 +7,7 @@ describe('adhashBidAdapter', function () { bidder: 'adhash', params: { publisherId: '0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb', - platformURL: 'https://adhash.org/p/struma/' + platformURL: 'https://adhash.com/p/struma/' }, mediaTypes: { banner: { @@ -115,18 +115,30 @@ describe('adhashBidAdapter', function () { adUnitCode: 'adunit-code', sizes: [[300, 250]], params: { - platformURL: 'https://adhash.org/p/struma/' + platformURL: 'https://adhash.com/p/struma/' } } }; + let bodyStub; + + const serverResponse = { + body: { + creatives: [{ costEUR: 1.234 }], + advertiserDomains: 'adhash.com', + badWords: [ + ['onqjbeq1', 'full', 1], + ['onqjbeq2', 'partial', 1], + ], + maxScore: 2 + } + }; + + afterEach(function() { + bodyStub && bodyStub.restore(); + }); + it('should interpret the response correctly', function () { - const serverResponse = { - body: { - creatives: [{ costEUR: 1.234 }], - advertiserDomains: 'adhash.org' - } - }; const result = spec.interpretResponse(serverResponse, request); expect(result.length).to.equal(1); expect(result[0].requestId).to.equal('12345678901234'); @@ -137,7 +149,49 @@ describe('adhashBidAdapter', function () { expect(result[0].netRevenue).to.equal(true); expect(result[0].currency).to.equal('EUR'); expect(result[0].ttl).to.equal(60); - expect(result[0].meta.advertiserDomains).to.eql(['adhash.org']); + expect(result[0].meta.advertiserDomains).to.eql(['adhash.com']); + }); + + it('should return empty array when there are bad words (full)', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return 'example text badWord1 badWord1 example badWord1 text'; + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); + }); + + it('should return empty array when there are bad words (partial)', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return 'example text partialBadWord2 badword2 example BadWord2text'; + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); + }); + + it('should return non-empty array when there are not enough bad words (full)', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return 'example text badWord1 badWord1 example text'; + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); + }); + + it('should return non-empty array when there are not enough bad words (partial)', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return 'example text partialBadWord2 example'; + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); + }); + + it('should return non-empty array when there are no-bad word matches', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return 'example text partialBadWord1 example text'; + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); + }); + + it('should return non-empty array when there is a problem with the brand-safety', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return null; + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return empty array when there are no creatives returned', function () { From d5e34de406a3a983e7394c5fe051e5997acff10f Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Thu, 10 Mar 2022 14:37:43 +0200 Subject: [PATCH 03/23] Fix for GDPR consent Removing the extra information as request data becomes too big and is sometimes truncated --- modules/adhashBidAdapter.js | 3 ++- test/spec/modules/adhashBidAdapter_spec.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 300f2e183e6..7b4641e7349 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -128,7 +128,8 @@ export const spec = { blockedCreatives: [], currentTimestamp: new Date().getTime(), recentAds: [], - GDPR: gdprConsent + GDPRApplies: gdprConsent ? gdprConsent.gdprApplies : null, + GDPR: gdprConsent ? gdprConsent.consentString : null }, options: { withCredentials: false, diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js index b30c32715c3..7dd5e0187ca 100644 --- a/test/spec/modules/adhashBidAdapter_spec.js +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -73,7 +73,7 @@ describe('adhashBidAdapter', function () { it('should build the request correctly', function () { const result = spec.buildRequests( [ bidRequest ], - { gdprConsent: true, refererInfo: { referer: 'http://example.com/' } } + { gdprConsent: { gdprApplies: true, consentString: 'example' }, refererInfo: { referer: 'http://example.com/' } } ); expect(result.length).to.equal(1); expect(result[0].method).to.equal('POST'); @@ -90,7 +90,7 @@ describe('adhashBidAdapter', function () { expect(result[0].data).to.have.property('recentAds'); }); it('should build the request correctly without referer', function () { - const result = spec.buildRequests([ bidRequest ], { gdprConsent: true }); + const result = spec.buildRequests([ bidRequest ], { gdprConsent: { gdprApplies: true, consentString: 'example' } }); expect(result.length).to.equal(1); expect(result[0].method).to.equal('POST'); expect(result[0].url).to.equal('https://bidder.adhash.com/rtb?version=1.0&prebid=true&publisher=0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb'); From bdc4fadd086caa7d05b92c1987e8851ee719d6ca Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Wed, 16 Mar 2022 09:56:07 +0200 Subject: [PATCH 04/23] Ad fraud prevention formula changed Ad fraud prevention formula changed to support negative values as well as linear distribution of article length --- modules/adhashBidAdapter.js | 10 +++++++--- test/spec/modules/adhashBidAdapter_spec.js | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index eeef1be9d20..7f5af047993 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -42,11 +42,16 @@ function brandSafety(badWords, maxScore) { * @returns {float} final score */ const scoreCalculator = (points, occurances) => { + let positive = true; + if (points < 0) { + points *= -1; + positive = false; + } let result = 0; for (let i = 0; i < occurances; i++) { result += Math.max(points - i * BAD_WORD_STEP, BAD_WORD_MIN); } - return result; + return positive ? result : -result; }; // Default parameters if the bidder is unable to send some of them @@ -57,7 +62,6 @@ function brandSafety(badWords, maxScore) { let score = 0; const content = window.top.document.body.innerText.toLowerCase(); const words = content.trim().split(/\s+/).length; - const factor = words < 500 ? 1 : words < 2000 ? 2 : 4; for (const [word, rule, points] of badWords) { if (rule === 'full' && new RegExp('\\b' + rot13(word) + '\\b', 'i').test(content)) { const occurances = content.match(new RegExp('\\b' + rot13(word) + '\\b', 'g')).length; @@ -67,7 +71,7 @@ function brandSafety(badWords, maxScore) { score += scoreCalculator(points, occurances); } } - return score < maxScore * factor; + return score < maxScore * words / 500; } catch (e) { return true; } diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js index 7dd5e0187ca..40bf354c4d9 100644 --- a/test/spec/modules/adhashBidAdapter_spec.js +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -129,6 +129,7 @@ describe('adhashBidAdapter', function () { badWords: [ ['onqjbeq1', 'full', 1], ['onqjbeq2', 'partial', 1], + ['tbbqjbeq', 'full', -1], ], maxScore: 2 } @@ -154,35 +155,42 @@ describe('adhashBidAdapter', function () { it('should return empty array when there are bad words (full)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text badWord1 badWord1 example badWord1 text'; + return 'example text badWord1 badWord1 example badWord1 text' + ' word'.repeat(493); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (partial)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text partialBadWord2 badword2 example BadWord2text'; + return 'example text partialBadWord2 badword2 example BadWord2text' + ' word'.repeat(494); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return non-empty array when there are not enough bad words (full)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text badWord1 badWord1 example text'; + return 'example text badWord1 badWord1 example text' + ' word'.repeat(494); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return non-empty array when there are not enough bad words (partial)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text partialBadWord2 example'; + return 'example text partialBadWord2 example' + ' word'.repeat(496); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return non-empty array when there are no-bad word matches', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text partialBadWord1 example text'; + return 'example text partialBadWord1 example text' + ' word'.repeat(495); + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); + }); + + it('should return non-empty array when there are bad words and good words', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return 'example text badWord1 badWord1 example badWord1 goodWord goodWord ' + ' word'.repeat(492); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); From 35f24dce18a577c01e9d07f3cb3a3d225ef8a69c Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Thu, 14 Apr 2022 11:16:50 +0300 Subject: [PATCH 05/23] AdHash brand safety additions Adding starts-with and ends-with rules that will help us with languages such as German where a single word can be written in multiple ways depending on the gender and grammatical case. --- modules/adhashBidAdapter.js | 16 ++++++++++++--- test/spec/modules/adhashBidAdapter_spec.js | 24 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 7f5af047993..a3ecb90b375 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -63,11 +63,21 @@ function brandSafety(badWords, maxScore) { const content = window.top.document.body.innerText.toLowerCase(); const words = content.trim().split(/\s+/).length; for (const [word, rule, points] of badWords) { - if (rule === 'full' && new RegExp('\\b' + rot13(word) + '\\b', 'i').test(content)) { - const occurances = content.match(new RegExp('\\b' + rot13(word) + '\\b', 'g')).length; + var decodedWord = rot13(word); + if (rule === 'full' && new RegExp('\\b' + decodedWord + '\\b', 'i').test(content)) { + const occurances = content.match(new RegExp('\\b' + decodedWord + '\\b', 'g')).length; score += scoreCalculator(points, occurances); } else if (rule === 'partial' && content.indexOf(rot13(word.toLowerCase())) > -1) { - const occurances = content.match(new RegExp(rot13(word), 'g')).length; + const occurances = content.match(new RegExp(decodedWord, 'g')).length; + score += scoreCalculator(points, occurances); + } else if (rule === 'starts' && new RegExp('\\b' + decodedWord, 'i').test(content)) { + const occurances = content.match(new RegExp('\\b' + decodedWord, 'g')).length; + score += scoreCalculator(points, occurances); + } else if (rule === 'ends' && new RegExp(decodedWord + '\\b', 'i').test(content)) { + const occurances = content.match(new RegExp(decodedWord + '\\b', 'g')).length; + score += scoreCalculator(points, occurances); + } else if (rule === 'regexp' && new RegExp(decodedWord, 'i').test(content)) { + const occurances = content.match(new RegExp(decodedWord, 'g')).length; score += scoreCalculator(points, occurances); } } diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js index 40bf354c4d9..3f729b4dd0c 100644 --- a/test/spec/modules/adhashBidAdapter_spec.js +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -130,6 +130,9 @@ describe('adhashBidAdapter', function () { ['onqjbeq1', 'full', 1], ['onqjbeq2', 'partial', 1], ['tbbqjbeq', 'full', -1], + ['fgnegf', 'starts', 1], + ['raqf', 'ends', 1], + ['kkk[no]lll', 'regexp', 1], ], maxScore: 2 } @@ -167,6 +170,27 @@ describe('adhashBidAdapter', function () { expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); + it('should return empty array when there are bad words (starts)', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return 'example text startsWith starts text startsAgain' + ' word'.repeat(494); + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); + }); + + it('should return empty array when there are bad words (ends)', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return 'example text wordEnds ends text anotherends' + ' word'.repeat(494); + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); + }); + + it('should return empty array when there are bad words (regexp)', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return 'example text xxxayyy zzxxxAyyyzz text xxxbyyy' + ' word'.repeat(494); + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); + }); + it('should return non-empty array when there are not enough bad words (full)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { return 'example text badWord1 badWord1 example text' + ' word'.repeat(494); From e681fbd8f5274b066cb71071768657e4e21e61e2 Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Tue, 28 Jun 2022 13:46:00 +0300 Subject: [PATCH 06/23] AdHash brand safety updates Added support for Cyrillic characters. Added support for bidderURL parameter. Fixed score multiplier from 500 to 1000. --- modules/adhashBidAdapter.js | 48 +++++++++++++++------- test/spec/modules/adhashBidAdapter_spec.js | 46 ++++++++++++++++----- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index a3ecb90b375..c7cfd0fe035 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -2,7 +2,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {includes} from '../src/polyfill.js'; import {BANNER} from '../src/mediaTypes.js'; -const VERSION = '1.0'; +const VERSION = '3.0'; const BAD_WORD_STEP = 0.1; const BAD_WORD_MIN = 0.2; @@ -62,26 +62,45 @@ function brandSafety(badWords, maxScore) { let score = 0; const content = window.top.document.body.innerText.toLowerCase(); const words = content.trim().split(/\s+/).length; + // Cyrillic unicode block range - 0400-04FF + const cyrillicWords = content.match(/[\u0400-\u04FF]+/gi); for (const [word, rule, points] of badWords) { - var decodedWord = rot13(word); - if (rule === 'full' && new RegExp('\\b' + decodedWord + '\\b', 'i').test(content)) { - const occurances = content.match(new RegExp('\\b' + decodedWord + '\\b', 'g')).length; + const decodedWord = rot13(word); + if ( + (rule === 'full' && new RegExp('\\b' + decodedWord + '\\b', 'i').test(content)) || + (rule === 'full' && cyrillicWords && cyrillicWords.includes(decodedWord)) + ) { + const occurances = cyrillicWords && cyrillicWords.includes(decodedWord) + ? cyrillicWords.filter(word => word === decodedWord).length + : content.match(new RegExp('\\b' + decodedWord + '\\b', 'g')).length; score += scoreCalculator(points, occurances); } else if (rule === 'partial' && content.indexOf(rot13(word.toLowerCase())) > -1) { const occurances = content.match(new RegExp(decodedWord, 'g')).length; score += scoreCalculator(points, occurances); - } else if (rule === 'starts' && new RegExp('\\b' + decodedWord, 'i').test(content)) { - const occurances = content.match(new RegExp('\\b' + decodedWord, 'g')).length; + } else if ( + (rule === 'starts' && new RegExp('\\b' + decodedWord, 'i').test(content)) || + (rule === 'starts' && cyrillicWords && cyrillicWords.some(word => word.startsWith(decodedWord))) + ) { + const occurances = + cyrillicWords && cyrillicWords.some(word => word.startsWith(decodedWord)) + ? cyrillicWords.find(word => word.startsWith(decodedWord)).length + : content.match(new RegExp('\\b' + decodedWord, 'g')).length; score += scoreCalculator(points, occurances); - } else if (rule === 'ends' && new RegExp(decodedWord + '\\b', 'i').test(content)) { - const occurances = content.match(new RegExp(decodedWord + '\\b', 'g')).length; + } else if ( + (rule === 'ends' && new RegExp(decodedWord + '\\b', 'i').test(content)) || + (rule === 'ends' && cyrillicWords && cyrillicWords.some(word => word.endsWith(decodedWord))) + ) { + const occurances = + cyrillicWords && cyrillicWords.some(word => word.endsWith(decodedWord)) + ? cyrillicWords.find(word => word.endsWith(decodedWord)).length + : content.match(new RegExp(decodedWord + '\\b', 'g')).length; score += scoreCalculator(points, occurances); } else if (rule === 'regexp' && new RegExp(decodedWord, 'i').test(content)) { const occurances = content.match(new RegExp(decodedWord, 'g')).length; score += scoreCalculator(points, occurances); } } - return score < maxScore * words / 500; + return score < maxScore * words / 1000; } catch (e) { return true; } @@ -89,7 +108,6 @@ function brandSafety(badWords, maxScore) { export const spec = { code: 'adhash', - url: 'https://bidder.adhash.com/rtb?version=' + VERSION + '&prebid=true', supportedMediaTypes: [ BANNER ], isBidRequestValid: (bid) => { @@ -109,15 +127,16 @@ export const spec = { buildRequests: (validBidRequests, bidderRequest) => { const { gdprConsent } = bidderRequest; - const { url } = spec; const bidRequests = []; let referrer = ''; if (bidderRequest && bidderRequest.refererInfo) { referrer = bidderRequest.refererInfo.referer; } for (var i = 0; i < validBidRequests.length; i++) { - var index = Math.floor(Math.random() * validBidRequests[i].sizes.length); - var size = validBidRequests[i].sizes[index].join('x'); + const bidderURL = validBidRequests[i].params.bidderURL || 'https://bidder.adhash.com'; + const url = `${bidderURL}/rtb?version=${VERSION}&prebid=true`; + const index = Math.floor(Math.random() * validBidRequests[i].sizes.length); + const size = validBidRequests[i].sizes[index].join('x'); bidRequests.push({ method: 'POST', url: url + '&publisher=' + validBidRequests[i].params.publisherId, @@ -166,6 +185,7 @@ export const spec = { } const publisherURL = JSON.stringify(request.bidRequest.params.platformURL); + const bidderURL = request.bidRequest.params.bidderURL || 'https://bidder.adhash.com'; const oneTimeId = request.bidRequest.adUnitCode + Math.random().toFixed(16).replace('0.', '.'); const bidderResponse = JSON.stringify({ responseText: JSON.stringify(responseBody) }); const requestData = JSON.stringify(request.data); @@ -175,7 +195,7 @@ export const spec = { cpm: responseBody.creatives[0].costEUR, ad: `
- + `, width: request.bidRequest.sizes[0][0], height: request.bidRequest.sizes[0][1], diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js index 3f729b4dd0c..57bfe5e76a3 100644 --- a/test/spec/modules/adhashBidAdapter_spec.js +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -77,7 +77,7 @@ describe('adhashBidAdapter', function () { ); expect(result.length).to.equal(1); expect(result[0].method).to.equal('POST'); - expect(result[0].url).to.equal('https://bidder.adhash.com/rtb?version=1.0&prebid=true&publisher=0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb'); + expect(result[0].url).to.equal('https://bidder.adhash.com/rtb?version=3.0&prebid=true&publisher=0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb'); expect(result[0].bidRequest).to.equal(bidRequest); expect(result[0].data).to.have.property('timezone'); expect(result[0].data).to.have.property('location'); @@ -93,7 +93,7 @@ describe('adhashBidAdapter', function () { const result = spec.buildRequests([ bidRequest ], { gdprConsent: { gdprApplies: true, consentString: 'example' } }); expect(result.length).to.equal(1); expect(result[0].method).to.equal('POST'); - expect(result[0].url).to.equal('https://bidder.adhash.com/rtb?version=1.0&prebid=true&publisher=0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb'); + expect(result[0].url).to.equal('https://bidder.adhash.com/rtb?version=3.0&prebid=true&publisher=0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb'); expect(result[0].bidRequest).to.equal(bidRequest); expect(result[0].data).to.have.property('timezone'); expect(result[0].data).to.have.property('location'); @@ -133,6 +133,9 @@ describe('adhashBidAdapter', function () { ['fgnegf', 'starts', 1], ['raqf', 'ends', 1], ['kkk[no]lll', 'regexp', 1], + ['дума', 'full', 1], + ['старт', 'starts', 1], + ['край', 'ends', 1], ], maxScore: 2 } @@ -158,63 +161,84 @@ describe('adhashBidAdapter', function () { it('should return empty array when there are bad words (full)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text badWord1 badWord1 example badWord1 text' + ' word'.repeat(493); + return 'example text badWord1 badWord1 example badWord1 text' + ' word'.repeat(993); + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); + }); + + it('should return empty array when there are bad words (full cyrillic)', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return 'example text дума дума example дума text' + ' текст'.repeat(993); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (partial)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text partialBadWord2 badword2 example BadWord2text' + ' word'.repeat(494); + return 'example text partialBadWord2 badword2 example BadWord2text' + ' word'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (starts)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text startsWith starts text startsAgain' + ' word'.repeat(494); + return 'example text startsWith starts text startsAgain' + ' word'.repeat(994); + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); + }); + + it('should return empty array when there are bad words (starts cyrillic)', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return 'example text стартТекст старт text стартТекст' + ' дума'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (ends)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text wordEnds ends text anotherends' + ' word'.repeat(494); + return 'example text wordEnds ends text anotherends' + ' word'.repeat(994); + }); + expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); + }); + + it('should return empty array when there are bad words (ends cyrillic)', function () { + bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + return 'example text ДругКрай край text ощеединкрай' + ' дума'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (regexp)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text xxxayyy zzxxxAyyyzz text xxxbyyy' + ' word'.repeat(494); + return 'example text xxxayyy zzxxxAyyyzz text xxxbyyy' + ' word'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return non-empty array when there are not enough bad words (full)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text badWord1 badWord1 example text' + ' word'.repeat(494); + return 'example text badWord1 badWord1 example text' + ' word'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return non-empty array when there are not enough bad words (partial)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text partialBadWord2 example' + ' word'.repeat(496); + return 'example text partialBadWord2 example' + ' word'.repeat(996); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return non-empty array when there are no-bad word matches', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text partialBadWord1 example text' + ' word'.repeat(495); + return 'example text partialBadWord1 example text' + ' word'.repeat(995); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return non-empty array when there are bad words and good words', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text badWord1 badWord1 example badWord1 goodWord goodWord ' + ' word'.repeat(492); + return 'example text badWord1 badWord1 example badWord1 goodWord goodWord ' + ' word'.repeat(992); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); From a6c5fafb9524dbeafb1a58d92d7e705caf0cfab7 Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Fri, 5 Aug 2022 11:41:21 +0300 Subject: [PATCH 07/23] AdHash Analytics adapter --- modules/adhashAnalyticsAdapter.js | 254 ++++++++++++++++++++++++++++++ modules/adhashBidAdapter.js | 6 +- 2 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 modules/adhashAnalyticsAdapter.js diff --git a/modules/adhashAnalyticsAdapter.js b/modules/adhashAnalyticsAdapter.js new file mode 100644 index 00000000000..ca6b293cdb7 --- /dev/null +++ b/modules/adhashAnalyticsAdapter.js @@ -0,0 +1,254 @@ +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; +import { ajax } from '../src/ajax.js'; +// import { config } from '../src/config.js'; + +const analyticsType = 'endpoint'; +const defaultUrl = ''; +const VERSION = '3.0'; +var auctionTracker = {}; +var bidTimeouts = []; +var auctionEndStorage = null; +var platformURL; +var bidderAnalyticsDomain; +var publisherId; + +const auctionInit = function (eventType, args) { + var auctionId = args.auctionId; + auctionTracker[auctionId] = {}; + // For each of the ad units, create the needed objects in auctionTracker + args.adUnitCodes.forEach( + adUnitId => auctionTracker[auctionId][adUnitId] = { + req: {}, + res: {}, + nob: {} + } + ); +} + +const bidRequested = function (eventType, args) { +} + +const bidResponse = function (eventType, args) { +} + +const bidWon = function (eventType, args) { + var relevantBidsData = []; + var responses = auctionTracker[args.auctionId][args.adUnitCode].res; + var nonResponses = auctionTracker[args.auctionId][args.adUnitCode].nob; + var bidResponders = Object.keys(responses); + var noBidResponders = Object.keys(nonResponses); + + var bidResponsesRaw = auctionTracker[args.auctionId][args.adUnitCode].res + var winningBid = {}; + var winningBidData = auctionEndStorage.bidsReceived.filter( + bid => bid.bidderCode === args.bidderCode && bid.adUnitCode === args.adUnitCode + )[0] + + winningBid.adTagId = args.adUnitCode; + winningBid.bid = true; + winningBid.language = window.navigator.language || ''; + winningBid.userAgent = window.navigator.userAgent || ''; + if (navigator.userAgentData && navigator.userAgentData.platform) { + winningBid.platform = navigator.userAgentData.platform; + } else { + winningBid.platform = navigator.platform; + } + winningBid.timeZone = new Date().getTimezoneOffset() / 60; + winningBid.width = winningBidData.width; + winningBid.height = winningBidData.height; + winningBid.screenWidth = screen.width; + winningBid.screenHeight = screen.height; + winningBid.size = `${winningBidData.width}x${winningBidData.height}`; + winningBid.win = true; + + winningBid.cost = args.cpm / 1000; + winningBid.currency = args.currency; + winningBid.delay = args.timeToRespond; + winningBid.domain = auctionTracker.domain; + winningBid.ssp = args.bidder; + + relevantBidsData.push(winningBid); + for (let bidder of bidResponders) { + if (bidResponsesRaw[bidder].ssp !== winningBid.ssp) { + relevantBidsData.push(bidResponsesRaw[bidder]); + } + } + + for (let bidder of noBidResponders) { + relevantBidsData.push(nonResponses[bidder]); + } + + // Send the JSON-stringified array to server + var payload = JSON.stringify(relevantBidsData); + var bidderPayload; + var platformUrlMatch = platformURL.match(/.+(?=protocol\.php)/) + var fullPlatformURL = (platformUrlMatch ? platformUrlMatch[0] : platformURL) + 'data.php?type=pbstats'; + + ajax(fullPlatformURL, null, payload); + if (bidderAnalyticsDomain && publisherId) { + var optionalForwardSlash = bidderAnalyticsDomain.match(/\/$/) ? '' : '/'; + var bidderAnalyticsURL = `${bidderAnalyticsDomain}${optionalForwardSlash}protocol.php?action=prebid_impression&version=${VERSION}` + + bidderPayload = JSON.stringify( + { + platform: publisherId, + data: relevantBidsData + } + ); + ajax(bidderAnalyticsURL, null, bidderPayload); + } +} + +const bidTimeout = function (eventType, args) { + bidTimeouts = args; +} + +const auctionEnd = function (eventType, args) { + auctionEndStorage = args; + // adding domain here: + if (!auctionTracker.domain) { + try { + auctionTracker.domain = window.top.location.host; + } catch (e) { + auctionTracker.domain = ''; + } + } + + // Populate Request info + args.bidderRequests.forEach(req => { + for (var bid of req.bids) { + auctionTracker[req.auctionId][bid.adUnitCode].req[req.bidderCode] = { + ssp: req.bidderCode, + domain: auctionTracker.domain, + delay: null, + bid: false, + win: false, + timeout: false, + cost: null + } + } + }) + + // Populate Response info + args.bidsReceived.forEach(res => { + var unitAuction = auctionTracker[res.auctionId][res.adUnitCode]; + unitAuction.res[res.bidderCode] = { + ssp: res.bidderCode, + domain: auctionTracker.domain, + delay: res.timeToRespond, + bid: true, + win: false, + timeout: false, + cost: res.cpm / 1000, + currency: res.currency + } + }) + + args.noBids.forEach(res => { + var unitAuction = auctionTracker[res.auctionId][res.adUnitCode]; + + var nobObj = unitAuction.nob; + nobObj[res.bidder] = { + ssp: res.bidder, + domain: auctionTracker.domain, + delay: null, + bid: false, + win: false, + timeout: false, + cost: 0.0, + } + }) + + bidTimeouts.forEach(req => { + var unitAuction = auctionTracker[req.auctionId][req.adUnitCode]; + var noBidObject = unitAuction.nob; + if (!noBidObject[req.bidder]) { + noBidObject[req.bidder] = { + ssp: req.bidder, + domain: auctionTracker.domain, + bid: false, + win: false, + timeout: true, + cost: 0.0, + } + } else { + noBidObject[req.bidder].timeout = true; + } + }) +} + +const noBid = function (eventType, args) { + var auctionId = args.auctionId; + var adUnitCode = args.adUnitCode; + var bidder = args.bidder; + auctionTracker[auctionId][adUnitCode].nob[bidder] = { + bid: false, + cost: 0, + domain: auctionTracker.domain, + ssp: bidder, + timeout: false, + win: false + } +} + +const { + EVENTS: { + AUCTION_INIT, + BID_REQUESTED, + BID_TIMEOUT, + BID_RESPONSE, + BID_WON, + AUCTION_END, + NO_BID + } +} = CONSTANTS; + +var adhashAdapter = Object.assign(adapter({ defaultUrl, analyticsType }), { + track({ eventType, args }) { + switch (eventType) { + case AUCTION_INIT: + auctionInit(eventType, args); + break; + case BID_REQUESTED: + bidRequested(eventType, args); + break; + case BID_RESPONSE: + bidResponse(eventType, args); + break; + case BID_WON: + bidWon(eventType, args); // Send the data here. + break; + case BID_TIMEOUT: + bidTimeout(eventType, args); + break; + case AUCTION_END: + auctionEnd(eventType, args); + break; + case NO_BID: + noBid(eventType, args); + break; + default: + break; + } + } +}); +adhashAdapter.context = {}; + +adhashAdapter.originEnableAnalytics = adhashAdapter.enableAnalytics; +adhashAdapter.enableAnalytics = (config) => { + adhashAdapter.initOptions = config.options; + platformURL = adhashAdapter.initOptions.platformURL; + bidderAnalyticsDomain = adhashAdapter.initOptions.bidderURL; + publisherId = adhashAdapter.initOptions.publisherId; + + adhashAdapter.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: adhashAdapter, + code: 'adhash' +}); + +export default adhashAdapter; diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index c7cfd0fe035..a73e4d9ab8e 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -129,8 +129,10 @@ export const spec = { const { gdprConsent } = bidderRequest; const bidRequests = []; let referrer = ''; - if (bidderRequest && bidderRequest.refererInfo) { - referrer = bidderRequest.refererInfo.referer; + try { + referrer = window.top.location.href; + } catch (e) { + referrer = window.location.href; } for (var i = 0; i < validBidRequests.length; i++) { const bidderURL = validBidRequests[i].params.bidderURL || 'https://bidder.adhash.com'; From 3fa0e52a380cbb01e45d401f5fffeaf4341b969b Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Wed, 17 Aug 2022 10:59:57 +0300 Subject: [PATCH 08/23] Support for recent ads Support for recent ads which gives us the option to do frequency and recency capping. --- modules/adhashBidAdapter.js | 14 ++++++++++++-- modules/adhashBidAdapter.md | 4 +--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index a73e4d9ab8e..9c5e6407eb4 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -1,10 +1,12 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; import {includes} from '../src/polyfill.js'; import {BANNER} from '../src/mediaTypes.js'; const VERSION = '3.0'; const BAD_WORD_STEP = 0.1; const BAD_WORD_MIN = 0.2; +const ADHASH_BIDDER_CODE = 'adhash'; /** * Function that checks the page where the ads are being served for brand safety. @@ -107,7 +109,7 @@ function brandSafety(badWords, maxScore) { } export const spec = { - code: 'adhash', + code: ADHASH_BIDDER_CODE, supportedMediaTypes: [ BANNER ], isBidRequestValid: (bid) => { @@ -126,6 +128,7 @@ export const spec = { }, buildRequests: (validBidRequests, bidderRequest) => { + const storage = getStorageManager({ bidderCode: ADHASH_BIDDER_CODE }); const { gdprConsent } = bidderRequest; const bidRequests = []; let referrer = ''; @@ -139,6 +142,13 @@ export const spec = { const url = `${bidderURL}/rtb?version=${VERSION}&prebid=true`; const index = Math.floor(Math.random() * validBidRequests[i].sizes.length); const size = validBidRequests[i].sizes[index].join('x'); + + let recentAds = []; + if (storage.localStorageIsEnabled()) { + const prefix = validBidRequests[i].params.prefix || 'adHash'; + recentAds = JSON.parse(storage.getDataFromLocalStorage(prefix + 'recentAds') || '[]'); + } + bidRequests.push({ method: 'POST', url: url + '&publisher=' + validBidRequests[i].params.publisherId, @@ -162,7 +172,7 @@ export const spec = { }], blockedCreatives: [], currentTimestamp: new Date().getTime(), - recentAds: [], + recentAds: recentAds, GDPRApplies: gdprConsent ? gdprConsent.gdprApplies : null, GDPR: gdprConsent ? gdprConsent.consentString : null }, diff --git a/modules/adhashBidAdapter.md b/modules/adhashBidAdapter.md index 4ee6ed3dc83..acca5a1e651 100644 --- a/modules/adhashBidAdapter.md +++ b/modules/adhashBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: AdHash Bidder Adapter Module Type: Bidder Adapter -Maintainer: damyan@adhash.org +Maintainer: damyan@adhash.com ``` # Description @@ -14,8 +14,6 @@ Here is what you need for Prebid integration with AdHash: 3. Use the Publisher ID and Platform URL as parameters in params. Please note that a number of AdHash functionalities are not supported in the Prebid.js integration: -* Cookie-less frequency and recency capping; -* Audience segments; * Price floors and passback tags, as they are not needed in the Prebid.js setup; * Reservation for direct deals only, as bids are evaluated based on their price. From 66fddd1318e0162c63b522b1934e287ecf1099cc Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Thu, 18 Aug 2022 10:08:13 +0300 Subject: [PATCH 09/23] Fix for timestamp --- modules/adhashBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 9c5e6407eb4..190c60d95a0 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -171,7 +171,7 @@ export const spec = { position: validBidRequests[i].adUnitCode }], blockedCreatives: [], - currentTimestamp: new Date().getTime(), + currentTimestamp: (new Date().getTime() / 1000) | 0, recentAds: recentAds, GDPRApplies: gdprConsent ? gdprConsent.gdprApplies : null, GDPR: gdprConsent ? gdprConsent.consentString : null From 0c7953c87bd4d33eead2bc0bc11dcc680f001d99 Mon Sep 17 00:00:00 2001 From: NikolayMGeorgiev Date: Thu, 15 Sep 2022 11:30:20 +0300 Subject: [PATCH 10/23] PUB-222 Added logic for measuring the fill rate (fallbacks) for Prebid impressions --- modules/adhashAnalyticsAdapter.js | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/modules/adhashAnalyticsAdapter.js b/modules/adhashAnalyticsAdapter.js index ca6b293cdb7..2a0356550a8 100644 --- a/modules/adhashAnalyticsAdapter.js +++ b/modules/adhashAnalyticsAdapter.js @@ -107,6 +107,15 @@ const bidTimeout = function (eventType, args) { const auctionEnd = function (eventType, args) { auctionEndStorage = args; + var adUnitsHaveBids = {}; + var adUnitsHaveAdhashFallback = {}; + var adUnitsHaveOtherFallbacks = {}; + for (var adUnit of args.adUnitCodes) { + adUnitsHaveBids[adUnit] = false; + adUnitsHaveAdhashFallback[adUnit] = false; + adUnitsHaveOtherFallbacks[adUnit] = false; + } + // adding domain here: if (!auctionTracker.domain) { try { @@ -144,6 +153,7 @@ const auctionEnd = function (eventType, args) { cost: res.cpm / 1000, currency: res.currency } + adUnitsHaveBids[res.adUnitCode] = true; }) args.noBids.forEach(res => { @@ -159,6 +169,11 @@ const auctionEnd = function (eventType, args) { timeout: false, cost: 0.0, } + if (res.bidder === 'adhash') { + adUnitsHaveAdhashFallback[res.adUnitCode] = true; + } else { + adUnitsHaveOtherFallbacks[res.adUnitCode] = true; + } }) bidTimeouts.forEach(req => { @@ -177,6 +192,34 @@ const auctionEnd = function (eventType, args) { noBidObject[req.bidder].timeout = true; } }) + + // Send fallback data for each ad unit + for (var adUnit of args.adUnitCodes) { + if (adUnitsHaveBids[adUnit]) { + continue; + } + var fallbackData = {}; + fallbackData.adTagId = adUnit; + fallbackData.pageURL = window.location.href; + if (navigator.userAgentData && navigator.userAgentData.platform) { + fallbackData.platform = navigator.userAgentData.platform; + } else { + fallbackData.platform = navigator.platform; + } + fallbackData.language = window.navigator.language || ''; + fallbackData.userAgent = window.navigator.userAgent || ''; + fallbackData.screenWidth = screen.width; + fallbackData.screenHeight = screen.height; + fallbackData.timeZone = new Date().getTimezoneOffset() / 60; + fallbackData.hasAdhashFallback = adUnitsHaveAdhashFallback[adUnit]; + fallbackData.hasOtherFallbacks = adUnitsHaveOtherFallbacks[adUnit]; + + var payload = JSON.stringify(fallbackData); + var platformUrlMatch = platformURL.match(/.+(?=protocol\.php)/); + var fullPlatformURL = (platformUrlMatch ? platformUrlMatch[0] : platformURL) + 'data.php?type=pbfallback'; + + ajax(fullPlatformURL, null, payload); + } } const noBid = function (eventType, args) { From 66d14ba004fa9490e7dc1f2d6191eef69f1732e0 Mon Sep 17 00:00:00 2001 From: NikolayMGeorgiev Date: Mon, 19 Sep 2022 12:10:20 +0300 Subject: [PATCH 11/23] Unit tests for the analytics adapter Added unit tests for the analytics adapter --- modules/adhashAnalyticsAdapter.js | 86 ++-- .../modules/adhashAnalyticsAdapter_spec.js | 436 ++++++++++++++++++ 2 files changed, 490 insertions(+), 32 deletions(-) create mode 100644 test/spec/modules/adhashAnalyticsAdapter_spec.js diff --git a/modules/adhashAnalyticsAdapter.js b/modules/adhashAnalyticsAdapter.js index 2a0356550a8..d1ac8447f06 100644 --- a/modules/adhashAnalyticsAdapter.js +++ b/modules/adhashAnalyticsAdapter.js @@ -13,8 +13,9 @@ var auctionEndStorage = null; var platformURL; var bidderAnalyticsDomain; var publisherId; +let fallbackSaver = {}; -const auctionInit = function (eventType, args) { +export const auctionInit = function (eventType, args) { var auctionId = args.auctionId; auctionTracker[auctionId] = {}; // For each of the ad units, create the needed objects in auctionTracker @@ -33,16 +34,16 @@ const bidRequested = function (eventType, args) { const bidResponse = function (eventType, args) { } -const bidWon = function (eventType, args) { - var relevantBidsData = []; - var responses = auctionTracker[args.auctionId][args.adUnitCode].res; - var nonResponses = auctionTracker[args.auctionId][args.adUnitCode].nob; - var bidResponders = Object.keys(responses); - var noBidResponders = Object.keys(nonResponses); +export const bidWon = function (eventType, args) { + let relevantBidsData = []; + let responses = auctionTracker[args.auctionId][args.adUnitCode].res; + let nonResponses = auctionTracker[args.auctionId][args.adUnitCode].nob; + let bidResponders = Object.keys(responses); + let noBidResponders = Object.keys(nonResponses); - var bidResponsesRaw = auctionTracker[args.auctionId][args.adUnitCode].res - var winningBid = {}; - var winningBidData = auctionEndStorage.bidsReceived.filter( + let bidResponsesRaw = auctionTracker[args.auctionId][args.adUnitCode].res + let winningBid = {}; + let winningBidData = auctionEndStorage.bidsReceived.filter( bid => bid.bidderCode === args.bidderCode && bid.adUnitCode === args.adUnitCode )[0] @@ -56,11 +57,11 @@ const bidWon = function (eventType, args) { winningBid.platform = navigator.platform; } winningBid.timeZone = new Date().getTimezoneOffset() / 60; - winningBid.width = winningBidData.width; - winningBid.height = winningBidData.height; + winningBid.width = winningBidData.width ? winningBidData.width : undefined; + winningBid.height = winningBidData.height ? winningBidData.height : undefined; winningBid.screenWidth = screen.width; winningBid.screenHeight = screen.height; - winningBid.size = `${winningBidData.width}x${winningBidData.height}`; + winningBid.size = winningBid.width && winningBid.height ? `${winningBidData.width}x${winningBidData.height}` : ''; winningBid.win = true; winningBid.cost = args.cpm / 1000; @@ -81,17 +82,16 @@ const bidWon = function (eventType, args) { } // Send the JSON-stringified array to server - var payload = JSON.stringify(relevantBidsData); - var bidderPayload; - var platformUrlMatch = platformURL.match(/.+(?=protocol\.php)/) - var fullPlatformURL = (platformUrlMatch ? platformUrlMatch[0] : platformURL) + 'data.php?type=pbstats'; + let payload = JSON.stringify(relevantBidsData); + let platformUrlMatch = platformURL.match(/.+(?=protocol\.php)/) + let fullPlatformURL = (platformUrlMatch ? platformUrlMatch[0] : platformURL) + 'data.php?type=pbstats'; ajax(fullPlatformURL, null, payload); if (bidderAnalyticsDomain && publisherId) { - var optionalForwardSlash = bidderAnalyticsDomain.match(/\/$/) ? '' : '/'; - var bidderAnalyticsURL = `${bidderAnalyticsDomain}${optionalForwardSlash}protocol.php?action=prebid_impression&version=${VERSION}` + let optionalForwardSlash = bidderAnalyticsDomain.match(/\/$/) ? '' : '/'; + let bidderAnalyticsURL = `${bidderAnalyticsDomain}${optionalForwardSlash}protocol.php?action=prebid_impression&version=${VERSION}` - bidderPayload = JSON.stringify( + let bidderPayload = JSON.stringify( { platform: publisherId, data: relevantBidsData @@ -101,16 +101,16 @@ const bidWon = function (eventType, args) { } } -const bidTimeout = function (eventType, args) { +export const bidTimeout = function (eventType, args) { bidTimeouts = args; } -const auctionEnd = function (eventType, args) { +export const auctionEnd = function (eventType, args) { auctionEndStorage = args; var adUnitsHaveBids = {}; var adUnitsHaveAdhashFallback = {}; var adUnitsHaveOtherFallbacks = {}; - for (var adUnit of args.adUnitCodes) { + for (let adUnit of args.adUnitCodes) { adUnitsHaveBids[adUnit] = false; adUnitsHaveAdhashFallback[adUnit] = false; adUnitsHaveOtherFallbacks[adUnit] = false; @@ -153,6 +153,10 @@ const auctionEnd = function (eventType, args) { cost: res.cpm / 1000, currency: res.currency } + if (res.width && res.height) { + unitAuction.res[res.bidderCode]['width'] = res.width; + unitAuction.res[res.bidderCode]['height'] = res.height; + } adUnitsHaveBids[res.adUnitCode] = true; }) @@ -194,11 +198,11 @@ const auctionEnd = function (eventType, args) { }) // Send fallback data for each ad unit - for (var adUnit of args.adUnitCodes) { + for (let adUnit of args.adUnitCodes) { if (adUnitsHaveBids[adUnit]) { continue; } - var fallbackData = {}; + let fallbackData = {}; fallbackData.adTagId = adUnit; fallbackData.pageURL = window.location.href; if (navigator.userAgentData && navigator.userAgentData.platform) { @@ -213,16 +217,17 @@ const auctionEnd = function (eventType, args) { fallbackData.timeZone = new Date().getTimezoneOffset() / 60; fallbackData.hasAdhashFallback = adUnitsHaveAdhashFallback[adUnit]; fallbackData.hasOtherFallbacks = adUnitsHaveOtherFallbacks[adUnit]; + fallbackSaver = fallbackData; - var payload = JSON.stringify(fallbackData); - var platformUrlMatch = platformURL.match(/.+(?=protocol\.php)/); - var fullPlatformURL = (platformUrlMatch ? platformUrlMatch[0] : platformURL) + 'data.php?type=pbfallback'; + let payload = JSON.stringify(fallbackData); + let platformUrlMatch = platformURL.match(/.+(?=protocol\.php)/); + let fullPlatformURL = (platformUrlMatch ? platformUrlMatch[0] : platformURL) + 'data.php?type=pbfallback'; ajax(fullPlatformURL, null, payload); } } -const noBid = function (eventType, args) { +export const noBid = function (eventType, args) { var auctionId = args.auctionId; var adUnitCode = args.adUnitCode; var bidder = args.bidder; @@ -236,7 +241,7 @@ const noBid = function (eventType, args) { } } -const { +export const { EVENTS: { AUCTION_INIT, BID_REQUESTED, @@ -248,7 +253,7 @@ const { } } = CONSTANTS; -var adhashAdapter = Object.assign(adapter({ defaultUrl, analyticsType }), { +let adhashAdapter = Object.assign(adapter({ defaultUrl, analyticsType }), { track({ eventType, args }) { switch (eventType) { case AUCTION_INIT: @@ -280,7 +285,7 @@ var adhashAdapter = Object.assign(adapter({ defaultUrl, analyticsType }), { adhashAdapter.context = {}; adhashAdapter.originEnableAnalytics = adhashAdapter.enableAnalytics; -adhashAdapter.enableAnalytics = (config) => { +adhashAdapter.enableAnalytics = function(config) { adhashAdapter.initOptions = config.options; platformURL = adhashAdapter.initOptions.platformURL; bidderAnalyticsDomain = adhashAdapter.initOptions.bidderURL; @@ -294,4 +299,21 @@ adapterManager.registerAnalyticsAdapter({ code: 'adhash' }); +// Functions needed for unit testing +export function getAuctionTracker () { + return auctionTracker; +} + +export function getTimeouts() { + return bidTimeouts; +} + +export function getSavedFallbackData() { + return fallbackSaver; +} + +export function clearSavedFallbackData() { + fallbackSaver = {}; +} + export default adhashAdapter; diff --git a/test/spec/modules/adhashAnalyticsAdapter_spec.js b/test/spec/modules/adhashAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..ac6dffde456 --- /dev/null +++ b/test/spec/modules/adhashAnalyticsAdapter_spec.js @@ -0,0 +1,436 @@ +import { expect } from 'chai'; +import adhashAdapter from 'modules/adhashAnalyticsAdapter.js'; +import { getAuctionTracker, getTimeouts, getSavedFallbackData,clearSavedFallbackData } from 'modules/adhashAnalyticsAdapter.js'; +import CONSTANTS from 'src/constants.json'; +import * as events from 'src/events.js'; + +const { + EVENTS: { + AUCTION_INIT, + BID_REQUESTED, + BID_TIMEOUT, + BID_RESPONSE, + BID_WON, + AUCTION_END, + NO_BID + } +} = CONSTANTS; + +const AD_UNIT_CODE = 'test-ad-unit'; +const AUCTION_ID = 'test-auction-id'; +const CPM_WIN = 5; +const CPM_LOSE = 4; +const CURRENT_TIME = 1663318800000; +const SLOT_LOAD_WAIT_TIME = 10; + +const ANALYTICS_CONFIG = { + publisherId: '0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb', + platformURL: 'https://adhash.com/p/struma/', + orgId: 'test-org-id', + publisherAccountId: 123, + publisherPlatformId: 'test-platform-id', + configId: 'my_config', + optimizerConfig: 'my my optimizer', + sample: 1.0, + payloadWaitTime: SLOT_LOAD_WAIT_TIME, + payloadWaitTimePadding: SLOT_LOAD_WAIT_TIME +}; + +const auctionInitArgs = { + auctionId: AUCTION_ID, + timestamp: CURRENT_TIME, + timeout: 3000, + adUnitCodes: [AD_UNIT_CODE], +}; + +const bidRequestedAdhashArgs = { + auctionId: AUCTION_ID, + bidderCode: 'adhash', + auctionStart: CURRENT_TIME, + timeout: 2000, + bids: [ + { + adUnitCode: AD_UNIT_CODE, + bidId: 'adhash-request-id', + params: { unit: 'adhash-ad-unit-id' }, + } + ], + start: CURRENT_TIME + 10 +}; + +const bidRequestedOpenXArgs = { + auctionId: AUCTION_ID, + bidderCode: 'openx', + auctionStart: CURRENT_TIME, + timeout: 1000, + bids: [ + { + adUnitCode: AD_UNIT_CODE, + bidId: 'openx-request-id', + params: { unit: 'openx-ad-unit-id' }, + } + ], + start: CURRENT_TIME + 20 +}; + +const bidResponseAdhashArgs = { + adUnitCode: AD_UNIT_CODE, + bidderCode: 'adhash', + cpm: CPM_WIN, + netRevenue: true, + requestId: 'adhash-request-id', + mediaType: 'banner', + width: 300, + height: 250, + adId: 'adhash-ad-id', + auctionId: AUCTION_ID, + creativeId: 'adhash-creative-id', + currency: 'BG', + timeToRespond: 100, + responseTimestamp: CURRENT_TIME + 30, + ts: 'test-adhash-ts' +}; + +const bidResponseOpenXArgs = { + adUnitCode: AD_UNIT_CODE, + bidderCode: 'openx', + cpm: CPM_LOSE, + netRevenue: true, + requestId: 'openx-request-id', + mediaType: 'banner', + width: 300, + height: 250, + adId: 'openx-ad-id', + auctionId: AUCTION_ID, + creativeId: 'openx-creative-id', + currency: 'BG', + timeToRespond: 100, + responseTimestamp: CURRENT_TIME + 40, + ts: 'test-openx-ts' +}; + +const bidRequestedRubiconArgs = { + auctionId: AUCTION_ID, + bidderCode: 'rubicon', + auctionStart: CURRENT_TIME, + timeout: 1000, + bids: [], + start: CURRENT_TIME + 50 +}; + +const noBidsRubiconArgs = { + adUnitCode: AD_UNIT_CODE, + auctionId: AUCTION_ID, + bidder: 'rubicon' +} + +const bidTimeoutRubiconArgs = { + adUnitCode: AD_UNIT_CODE, + auctionId: AUCTION_ID, + bidder: 'rubicon' +} + +const bidWonAdhashArgs = { + adUnitCode: AD_UNIT_CODE, + auctionId: AUCTION_ID, + bidderCode: 'adhash', + requestId: 'adhash-request-id', + adId: 'adhash-ad-id' +}; + +const auctionEndArgs = { + auctionId: AUCTION_ID, + timestamp: CURRENT_TIME, + auctionEnd: CURRENT_TIME + 100, + timeout: 3000, + adUnitCodes: [AD_UNIT_CODE], +}; + +describe('adhashAnalyticsAdapter', function () { + let clock; + + beforeEach(function() { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers(CURRENT_TIME); + }); + + afterEach(function() { + events.getEvents.restore(); + clock.restore(); + }); + + describe('auctionInit', function () { + adhashAdapter.enableAnalytics({options: ANALYTICS_CONFIG}); + + let clock = sinon.useFakeTimers(CURRENT_TIME); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + + it('should initialize the auction tracker with empty data for each ad unit', function() { + simulateAuction([ + [AUCTION_INIT, auctionInitArgs] + ]); + let auctionTracker = getAuctionTracker(); + var unitIsInitialized = auctionTracker[AUCTION_ID] != undefined && auctionTracker[AUCTION_ID][[auctionInitArgs.adUnitCodes[0]]] != undefined; + expect(unitIsInitialized).is.true; + }); + + adhashAdapter.disableAnalytics(); + }); + + describe('auctionEnd with fallback', function () { + let clock = sinon.useFakeTimers(CURRENT_TIME); + + beforeEach(function () { + adhashAdapter.enableAnalytics({options: ANALYTICS_CONFIG}); + + simulateAuction([ + [AUCTION_INIT, auctionInitArgs], + [BID_REQUESTED, bidRequestedRubiconArgs], + [NO_BID, noBidsRubiconArgs], + [AUCTION_END, auctionEndArgs] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + }); + + afterEach(function () { + adhashAdapter.disableAnalytics(); + }); + + it('should have fallback data', function () { + let fallbackData = getSavedFallbackData(); + expect(fallbackData).to.contain({adTagId: AD_UNIT_CODE}); + clearSavedFallbackData(); + }); + }); + + describe('auctionEnd', function () { + let adhashBidResponse; + let openxBidResponse; + let auctionTracker; + + let clock = sinon.useFakeTimers(CURRENT_TIME); + beforeEach(function () { + adhashAdapter.enableAnalytics({options: ANALYTICS_CONFIG}); + + simulateAuction([ + [AUCTION_INIT, auctionInitArgs], + [BID_REQUESTED, bidRequestedAdhashArgs], + [BID_REQUESTED, bidRequestedOpenXArgs], + [BID_RESPONSE, bidResponseAdhashArgs], + [BID_RESPONSE, bidResponseOpenXArgs], + [BID_REQUESTED, bidRequestedRubiconArgs], + [AUCTION_END, auctionEndArgs] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + + auctionTracker = getAuctionTracker(); + adhashBidResponse = auctionTracker[AUCTION_ID][AD_UNIT_CODE]['res']['adhash']; + openxBidResponse = auctionTracker[AUCTION_ID][AD_UNIT_CODE]['res']['openx']; + }); + + afterEach(function () { + adhashAdapter.disableAnalytics(); + }); + + it('should have a cost for a single impression for every bid based on the CPM', function () { + let adhashCost = adhashBidResponse.cost ? adhashBidResponse.cost : 0; + let openxCost = openxBidResponse.cost ? openxBidResponse.cost : 0; + let adhashCostDifference = Math.abs(adhashCost - (CPM_WIN / 1000)); + let openxCostDifference = Math.abs(openxCost - (CPM_LOSE / 1000)); + expect(adhashBidResponse.cost).is.not.undefined; + expect(openxBidResponse.cost).is.not.undefined; + expect(adhashCostDifference).lt(Number.EPSILON); + expect(openxCostDifference).lt(Number.EPSILON); + }); + + it('should track the currency', function () { + expect(adhashBidResponse.currency).to.equal(bidResponseAdhashArgs.currency); + expect(openxBidResponse.currency).to.equal(bidResponseOpenXArgs.currency); + }); + + it('should track the bid\'s latency', function () { + expect(adhashBidResponse.delay).to.equal(bidResponseAdhashArgs.timeToRespond); + expect(openxBidResponse.delay).to.equal(bidResponseOpenXArgs.timeToRespond); + }); + + it('should not have any bid winners', function () { + expect(adhashBidResponse.win).to.equal(false); + expect(openxBidResponse.win).to.equal(false); + }); + }); + + describe('bidWon', function () { + let adhashBidResponse; + let openxBidResponse; + let auctionTracker; + + let clock = sinon.useFakeTimers(CURRENT_TIME); + beforeEach(function () { + adhashAdapter.enableAnalytics({options: ANALYTICS_CONFIG}); + + simulateAuction([ + [AUCTION_INIT, auctionInitArgs], + [BID_REQUESTED, bidRequestedAdhashArgs], + [BID_REQUESTED, bidRequestedOpenXArgs], + [BID_RESPONSE, bidResponseAdhashArgs], + [BID_RESPONSE, bidResponseOpenXArgs], + [AUCTION_END, auctionEndArgs], + [BID_WON, bidWonAdhashArgs] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + + auctionTracker = getAuctionTracker(); + adhashBidResponse = auctionTracker[AUCTION_ID][AD_UNIT_CODE]['res']['adhash']; + openxBidResponse = auctionTracker[AUCTION_ID][AD_UNIT_CODE]['res']['openx']; + }); + + afterEach(function () { + adhashAdapter.disableAnalytics(); + }); + + it('should have a winning bidder that was marked as having a bid', function () { + expect(adhashBidResponse).to.contain({bid: true}); + }); + + it('should mark the rest of the bidders as the losers', function () { + expect(openxBidResponse).to.contain({win: false}); + }); + }); + + describe('noBid', function () { + let auctionTracker; + let clock = sinon.useFakeTimers(CURRENT_TIME); + + beforeEach(function () { + adhashAdapter.enableAnalytics({options: ANALYTICS_CONFIG}); + + simulateAuction([ + [AUCTION_INIT, auctionInitArgs], + [BID_REQUESTED, bidRequestedOpenXArgs], + [BID_RESPONSE, bidResponseOpenXArgs], + [BID_REQUESTED, bidRequestedRubiconArgs], + [NO_BID, noBidsRubiconArgs], + [AUCTION_END, auctionEndArgs] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auctionTracker = getAuctionTracker(); + }); + + afterEach(function () { + adhashAdapter.disableAnalytics(); + }); + + it('should add the responses with no bids to the auction tracker', function() { + expect(auctionTracker[AUCTION_ID][AD_UNIT_CODE].nob['rubicon']).is.not.undefined; + }); + + it('should not add the responses with bids to the responses with no bids', function() { + expect(auctionTracker[AUCTION_ID][AD_UNIT_CODE].nob['openx']).is.undefined; + }); + + it('should mark the responses with no bids as not winning', function() { + expect(auctionTracker[AUCTION_ID][AD_UNIT_CODE].nob['rubicon']['bid']).is.false; + expect(auctionTracker[AUCTION_ID][AD_UNIT_CODE].nob['rubicon']['win']).is.false; + }); + }); + + describe('bidTimeout', function () { + let clock = sinon.useFakeTimers(CURRENT_TIME); + + beforeEach(function () { + adhashAdapter.enableAnalytics({options: ANALYTICS_CONFIG}); + }); + + afterEach(function () { + adhashAdapter.disableAnalytics(); + }); + + it('should add the bid timeout data to the auction tracker', function () { + simulateAuction([ + [AUCTION_INIT, auctionInitArgs], + [BID_REQUESTED, bidRequestedRubiconArgs], + [BID_TIMEOUT, [bidTimeoutRubiconArgs]], + [AUCTION_END, auctionEndArgs] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + + let timeoutData = getTimeouts(); + for (let singleTimeout of timeoutData) { + expect(singleTimeout).to.contain({adUnitCode: AD_UNIT_CODE}); + } + }); + + it('should mark the bid with timeout as such when it hasn\'t been added to the bidders with no bids', function () { + simulateAuction([ + [AUCTION_INIT, auctionInitArgs], + [BID_REQUESTED, bidRequestedRubiconArgs], + [BID_TIMEOUT, [bidTimeoutRubiconArgs]], + [AUCTION_END, auctionEndArgs] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + + let auctionTracker = getAuctionTracker(); + expect(auctionTracker[AUCTION_ID][AD_UNIT_CODE]['nob'][bidTimeoutRubiconArgs.bidder]).to.contain({timeout: true}); + }); + + it('should mark the bid with timeout as such when it has already been added to the bidders with no bids', function () { + simulateAuction([ + [AUCTION_INIT, auctionInitArgs], + [BID_REQUESTED, bidRequestedRubiconArgs], + [NO_BID, noBidsRubiconArgs], + [BID_TIMEOUT, [bidTimeoutRubiconArgs]], + [AUCTION_END, auctionEndArgs] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + + let auctionTracker = getAuctionTracker(); + expect(auctionTracker[AUCTION_ID][AD_UNIT_CODE]['nob'][bidTimeoutRubiconArgs.bidder]).to.contain({timeout: true}); + }); + }); + + function simulateAuction(events) { + let highestBid; + let bidRequests = []; + let bidResponses = []; + let noBids = []; + let allArgs = {}; + + for (let event of events) { + const [eventType, args] = event; + if (eventType == BID_RESPONSE) { + highestBid = highestBid || args; + if (highestBid.cpm < args.cpm) { + highestBid = args; + } + bidResponses.push(args); + allArgs['bidsReceived'] = bidResponses; + } else if (eventType == BID_REQUESTED) { + bidRequests.push(args); + allArgs['bidderRequests'] = bidRequests; + } else if (eventType == NO_BID) { + noBids.push(args); + adhashAdapter.track({ eventType, args }); + continue; + } else if (eventType == BID_WON || eventType == BID_TIMEOUT) { + adhashAdapter.track({ eventType, args }); + continue; + } else if (eventType == AUCTION_INIT) { + allArgs = args; + } else if (eventType == AUCTION_END) { + allArgs['noBids'] = noBids; + if (!allArgs['bidsReceived']) { + allArgs['bidsReceived'] = []; + } + } + + adhashAdapter.track({ eventType, args: allArgs }); + }; + } +}); From 1c2470a0b7c9194006ae168da41a7d8e7417c552 Mon Sep 17 00:00:00 2001 From: NikolayMGeorgiev Date: Mon, 19 Sep 2022 18:44:32 +0300 Subject: [PATCH 12/23] Removed export causing errors Removed an unneeded export of a const that was causing errors with the analytics adapter --- modules/adhashAnalyticsAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/adhashAnalyticsAdapter.js b/modules/adhashAnalyticsAdapter.js index d1ac8447f06..7abb71aa1c9 100644 --- a/modules/adhashAnalyticsAdapter.js +++ b/modules/adhashAnalyticsAdapter.js @@ -241,7 +241,7 @@ export const noBid = function (eventType, args) { } } -export const { +const { EVENTS: { AUCTION_INIT, BID_REQUESTED, @@ -285,7 +285,7 @@ let adhashAdapter = Object.assign(adapter({ defaultUrl, analyticsType }), { adhashAdapter.context = {}; adhashAdapter.originEnableAnalytics = adhashAdapter.enableAnalytics; -adhashAdapter.enableAnalytics = function(config) { +adhashAdapter.enableAnalytics = (config) => { adhashAdapter.initOptions = config.options; platformURL = adhashAdapter.initOptions.platformURL; bidderAnalyticsDomain = adhashAdapter.initOptions.bidderURL; From 6e4148b33a87504f3b61c154f8e5a58eac9823f9 Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Mon, 26 Sep 2022 14:06:25 +0300 Subject: [PATCH 13/23] Added globalScript parameter --- modules/adhashBidAdapter.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 190c60d95a0..1782c211a34 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -199,6 +199,9 @@ export const spec = { const publisherURL = JSON.stringify(request.bidRequest.params.platformURL); const bidderURL = request.bidRequest.params.bidderURL || 'https://bidder.adhash.com'; const oneTimeId = request.bidRequest.adUnitCode + Math.random().toFixed(16).replace('0.', '.'); + const globalScript = !request.bidRequest.params.globalScript + ? `` + : ''; const bidderResponse = JSON.stringify({ responseText: JSON.stringify(responseBody) }); const requestData = JSON.stringify(request.data); @@ -206,8 +209,7 @@ export const spec = { requestId: request.bidRequest.bidId, cpm: responseBody.creatives[0].costEUR, ad: - `
- + `
${globalScript} `, width: request.bidRequest.sizes[0][0], height: request.bidRequest.sizes[0][1], From 441240df1251967bc7bc173b1f66657041499fda Mon Sep 17 00:00:00 2001 From: Ventsislav Saraminev Date: Tue, 4 Oct 2022 11:42:18 +0300 Subject: [PATCH 14/23] PUB-227 Support for non-latin and non-cyrillic symbols --- modules/adhashBidAdapter.js | 47 ++++++++++++------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 1782c211a34..98169829d0e 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -63,46 +63,29 @@ function brandSafety(badWords, maxScore) { try { let score = 0; const content = window.top.document.body.innerText.toLowerCase(); - const words = content.trim().split(/\s+/).length; - // Cyrillic unicode block range - 0400-04FF - const cyrillicWords = content.match(/[\u0400-\u04FF]+/gi); + const contentWords = content.trim().split(/\s+/).length; + // \p{L} matches a single unicode code point in the category 'letter'. Matches any kind of letter from any language. + const words = content.match(/[\p{L}-]+/ug); for (const [word, rule, points] of badWords) { - const decodedWord = rot13(word); - if ( - (rule === 'full' && new RegExp('\\b' + decodedWord + '\\b', 'i').test(content)) || - (rule === 'full' && cyrillicWords && cyrillicWords.includes(decodedWord)) - ) { - const occurances = cyrillicWords && cyrillicWords.includes(decodedWord) - ? cyrillicWords.filter(word => word === decodedWord).length - : content.match(new RegExp('\\b' + decodedWord + '\\b', 'g')).length; + const decodedWord = rot13(word.toLowerCase()); + if (rule === 'full' && words && words.includes(decodedWord)) { + const occurances = words.filter(word => word === decodedWord).length; score += scoreCalculator(points, occurances); - } else if (rule === 'partial' && content.indexOf(rot13(word.toLowerCase())) > -1) { - const occurances = content.match(new RegExp(decodedWord, 'g')).length; + } else if (rule === 'partial' && words && words.some(word => word.indexOf(decodedWord) > -1)) { + const occurances = words.filter(word => word.indexOf(decodedWord) > -1).length; score += scoreCalculator(points, occurances); - } else if ( - (rule === 'starts' && new RegExp('\\b' + decodedWord, 'i').test(content)) || - (rule === 'starts' && cyrillicWords && cyrillicWords.some(word => word.startsWith(decodedWord))) - ) { - const occurances = - cyrillicWords && cyrillicWords.some(word => word.startsWith(decodedWord)) - ? cyrillicWords.find(word => word.startsWith(decodedWord)).length - : content.match(new RegExp('\\b' + decodedWord, 'g')).length; + } else if (rule === 'starts' && words && words.some(word => word.startsWith(decodedWord))) { + const occurances = words.find(word => word.startsWith(decodedWord)).length; score += scoreCalculator(points, occurances); - } else if ( - (rule === 'ends' && new RegExp(decodedWord + '\\b', 'i').test(content)) || - (rule === 'ends' && cyrillicWords && cyrillicWords.some(word => word.endsWith(decodedWord))) - ) { - const occurances = - cyrillicWords && cyrillicWords.some(word => word.endsWith(decodedWord)) - ? cyrillicWords.find(word => word.endsWith(decodedWord)).length - : content.match(new RegExp(decodedWord + '\\b', 'g')).length; + } else if (rule === 'ends' && words && words.some(word => word.endsWith(decodedWord))) { + const occurances = words.find(word => word.endsWith(decodedWord)).length; score += scoreCalculator(points, occurances); - } else if (rule === 'regexp' && new RegExp(decodedWord, 'i').test(content)) { - const occurances = content.match(new RegExp(decodedWord, 'g')).length; + } else if (rule === 'regexp' && words && words.includes(decodedWord)) { + const occurances = words.filter(word => word === decodedWord).length; score += scoreCalculator(points, occurances); } } - return score < maxScore * words / 1000; + return score < maxScore * contentWords / 1000; } catch (e) { return true; } From 8dacbc9ceec93076756b154af5f4f53f271d3cd4 Mon Sep 17 00:00:00 2001 From: Dimitar Kalenderov Date: Fri, 28 Oct 2022 15:32:24 +0300 Subject: [PATCH 15/23] GEN-964 - Brand safety now checks the page URL for bad words. No ad is shown if there is at least one match. - Repeating code is optimized and moved to helper function - Multi-language support for brand safety --- modules/adhashBidAdapter.js | 77 +++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 98169829d0e..f37072f6e77 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -1,7 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; -import {includes} from '../src/polyfill.js'; -import {BANNER} from '../src/mediaTypes.js'; +import { includes } from '../src/polyfill.js'; +import { BANNER } from '../src/mediaTypes.js'; const VERSION = '3.0'; const BAD_WORD_STEP = 0.1; @@ -56,36 +56,73 @@ function brandSafety(badWords, maxScore) { return positive ? result : -result; }; + /** + * Checks what rule will match in the given array with words + * @param {string} rule rule type (full, partial, starts, ends, regexp) + * @param {string} decodedWord decoded word + * @param {array} wordsToMatch array to find a match + * @returns {object|boolean} matched rule and occurances. If nothing is matched returns false + */ + const wordsMatchedWithRule = function (rule, decodedWord, wordsToMatch) { + if (rule === "full" && wordsToMatch && wordsToMatch.includes(decodedWord)) { + return { rule, occurances: wordsToMatch.filter(element => element === decodedWord).length }; + } else if (rule === "partial" && wordsToMatch && wordsToMatch.some(element => element.indexOf(decodedWord) > -1)) { + return { rule, occurances: wordsToMatch.filter(element => element.indexOf(decodedWord) > -1).length }; + } else if (rule === "starts" && wordsToMatch && wordsToMatch.some(word => word.startsWith(decodedWord))) { + return { rule, occurances: wordsToMatch.filter(element => element.startsWith(decodedWord)).length }; + } else if (rule === "ends" && wordsToMatch && wordsToMatch.some(word => word.endsWith(decodedWord))) { + return { rule, occurances: wordsToMatch.filter(element => element.endsWith(decodedWord)).length }; + } else if (rule === "regexp" && wordsToMatch && wordsToMatch.includes(decodedWord)) { + return { rule, occurances: wordsToMatch.filter(element => element === decodedWord).length }; + } + return false; + }; + // Default parameters if the bidder is unable to send some of them badWords = badWords || []; maxScore = parseInt(maxScore) || 10; try { let score = 0; + const decodedUrl = decodeURI(window.top.location.href.substring(window.top.location.origin.length)); + const wordsAndNumbersInUrl = decodedUrl + .replaceAll(/[-,\._/\?=&#%]/g, ' ') + .replaceAll(/\s\s+/g, ' ') + .toLowerCase() + .trim(); const content = window.top.document.body.innerText.toLowerCase(); const contentWords = content.trim().split(/\s+/).length; // \p{L} matches a single unicode code point in the category 'letter'. Matches any kind of letter from any language. - const words = content.match(/[\p{L}-]+/ug); + const regexp = new RegExp('[\\p{L}]+', 'gu'); + const words = content.match(regexp); + const wordsInUrl = wordsAndNumbersInUrl.match(regexp); + for (const [word, rule, points] of badWords) { const decodedWord = rot13(word.toLowerCase()); - if (rule === 'full' && words && words.includes(decodedWord)) { - const occurances = words.filter(word => word === decodedWord).length; - score += scoreCalculator(points, occurances); - } else if (rule === 'partial' && words && words.some(word => word.indexOf(decodedWord) > -1)) { - const occurances = words.filter(word => word.indexOf(decodedWord) > -1).length; - score += scoreCalculator(points, occurances); - } else if (rule === 'starts' && words && words.some(word => word.startsWith(decodedWord))) { - const occurances = words.find(word => word.startsWith(decodedWord)).length; - score += scoreCalculator(points, occurances); - } else if (rule === 'ends' && words && words.some(word => word.endsWith(decodedWord))) { - const occurances = words.find(word => word.endsWith(decodedWord)).length; - score += scoreCalculator(points, occurances); - } else if (rule === 'regexp' && words && words.includes(decodedWord)) { - const occurances = words.filter(word => word === decodedWord).length; - score += scoreCalculator(points, occurances); + + // Checks the words in the url of the page only for negative words. Don't serve any ad when at least one match is found + if (points > 0) { + const matchedRuleInUrl = wordsMatchedWithRule(rule, decodedWord, wordsInUrl); + if (matchedRuleInUrl.rule) { + return false; + } + } + + // Check if site content's words match any of our brand safety rules + const matchedRule = wordsMatchedWithRule(rule, decodedWord, words); + if (matchedRule.rule === 'full') { + score += scoreCalculator(points, matchedRule.occurances); + } else if (matchedRule.rule === 'partial') { + score += scoreCalculator(points, matchedRule.occurances); + } else if (matchedRule.rule === 'starts') { + score += scoreCalculator(points, matchedRule.occurances); + } else if (matchedRule.rule === 'ends') { + score += scoreCalculator(points, matchedRule.occurances); + } else if (matchedRule.rule === 'regexp') { + score += scoreCalculator(points, matchedRule.occurances); } } - return score < maxScore * contentWords / 1000; + return score < (maxScore * contentWords) / 1000; } catch (e) { return true; } @@ -120,7 +157,7 @@ export const spec = { } catch (e) { referrer = window.location.href; } - for (var i = 0; i < validBidRequests.length; i++) { + for (const i = 0; i < validBidRequests.length; i++) { const bidderURL = validBidRequests[i].params.bidderURL || 'https://bidder.adhash.com'; const url = `${bidderURL}/rtb?version=${VERSION}&prebid=true`; const index = Math.floor(Math.random() * validBidRequests[i].sizes.length); From 9cd75bf1bbb697e6939b4540183da2684b45533d Mon Sep 17 00:00:00 2001 From: NikolayMGeorgiev Date: Wed, 16 Nov 2022 16:18:23 +0200 Subject: [PATCH 16/23] GEN-1025 Sending the needed ad density data to the bidder --- modules/adhashBidAdapter.js | 39 +++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index f37072f6e77..30f70a5b43c 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -64,15 +64,15 @@ function brandSafety(badWords, maxScore) { * @returns {object|boolean} matched rule and occurances. If nothing is matched returns false */ const wordsMatchedWithRule = function (rule, decodedWord, wordsToMatch) { - if (rule === "full" && wordsToMatch && wordsToMatch.includes(decodedWord)) { + if (rule === 'full' && wordsToMatch && wordsToMatch.includes(decodedWord)) { return { rule, occurances: wordsToMatch.filter(element => element === decodedWord).length }; - } else if (rule === "partial" && wordsToMatch && wordsToMatch.some(element => element.indexOf(decodedWord) > -1)) { + } else if (rule === 'partial' && wordsToMatch && wordsToMatch.some(element => element.indexOf(decodedWord) > -1)) { return { rule, occurances: wordsToMatch.filter(element => element.indexOf(decodedWord) > -1).length }; - } else if (rule === "starts" && wordsToMatch && wordsToMatch.some(word => word.startsWith(decodedWord))) { + } else if (rule === 'starts' && wordsToMatch && wordsToMatch.some(word => word.startsWith(decodedWord))) { return { rule, occurances: wordsToMatch.filter(element => element.startsWith(decodedWord)).length }; - } else if (rule === "ends" && wordsToMatch && wordsToMatch.some(word => word.endsWith(decodedWord))) { + } else if (rule === 'ends' && wordsToMatch && wordsToMatch.some(word => word.endsWith(decodedWord))) { return { rule, occurances: wordsToMatch.filter(element => element.endsWith(decodedWord)).length }; - } else if (rule === "regexp" && wordsToMatch && wordsToMatch.includes(decodedWord)) { + } else if (rule === 'regexp' && wordsToMatch && wordsToMatch.includes(decodedWord)) { return { rule, occurances: wordsToMatch.filter(element => element === decodedWord).length }; } return false; @@ -157,6 +157,17 @@ export const spec = { } catch (e) { referrer = window.location.href; } + var body = document.body; + var html = document.documentElement; + var pageHeight = Math.max( + body.scrollHeight, + body.offsetHeight, + html.clientHeight, + html.scrollHeight, + html.offsetHeight + ); + var pageWidth = Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth); + for (const i = 0; i < validBidRequests.length; i++) { const bidderURL = validBidRequests[i].params.bidderURL || 'https://bidder.adhash.com'; const url = `${bidderURL}/rtb?version=${VERSION}&prebid=true`; @@ -169,6 +180,18 @@ export const spec = { recentAds = JSON.parse(storage.getDataFromLocalStorage(prefix + 'recentAds') || '[]'); } + // Needed for the ad density calculation + var adHeight = validBidRequests[i].sizes[index][1]; + var adWidth = validBidRequests[i].sizes[index][0]; + if (!window.adsCount) { + window.adsCount = 0; + } + if (!window.adsTotalSurface) { + window.adsTotalSurface = 0; + } + window.adsTotalSurface += adHeight * adWidth; + window.adsCount++; + bidRequests.push({ method: 'POST', url: url + '&publisher=' + validBidRequests[i].params.publisherId, @@ -194,7 +217,11 @@ export const spec = { currentTimestamp: (new Date().getTime() / 1000) | 0, recentAds: recentAds, GDPRApplies: gdprConsent ? gdprConsent.gdprApplies : null, - GDPR: gdprConsent ? gdprConsent.consentString : null + GDPR: gdprConsent ? gdprConsent.consentString : null, + servedAdsCount: window.adsCount, + adsTotalSurface: window.adsTotalSurface, + pageHeight: pageHeight, + pageWidth: pageWidth }, options: { withCredentials: false, From dbd9f3d2e0fa0f3ed2f97cefa16ee6837e243a62 Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Mon, 28 Nov 2022 09:26:21 +0200 Subject: [PATCH 17/23] Removing the analytics adaptor --- modules/adhashAnalyticsAdapter.js | 319 ------------- .../modules/adhashAnalyticsAdapter_spec.js | 436 ------------------ 2 files changed, 755 deletions(-) delete mode 100644 modules/adhashAnalyticsAdapter.js delete mode 100644 test/spec/modules/adhashAnalyticsAdapter_spec.js diff --git a/modules/adhashAnalyticsAdapter.js b/modules/adhashAnalyticsAdapter.js deleted file mode 100644 index 7abb71aa1c9..00000000000 --- a/modules/adhashAnalyticsAdapter.js +++ /dev/null @@ -1,319 +0,0 @@ -import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; -import adapterManager from '../src/adapterManager.js'; -import CONSTANTS from '../src/constants.json'; -import { ajax } from '../src/ajax.js'; -// import { config } from '../src/config.js'; - -const analyticsType = 'endpoint'; -const defaultUrl = ''; -const VERSION = '3.0'; -var auctionTracker = {}; -var bidTimeouts = []; -var auctionEndStorage = null; -var platformURL; -var bidderAnalyticsDomain; -var publisherId; -let fallbackSaver = {}; - -export const auctionInit = function (eventType, args) { - var auctionId = args.auctionId; - auctionTracker[auctionId] = {}; - // For each of the ad units, create the needed objects in auctionTracker - args.adUnitCodes.forEach( - adUnitId => auctionTracker[auctionId][adUnitId] = { - req: {}, - res: {}, - nob: {} - } - ); -} - -const bidRequested = function (eventType, args) { -} - -const bidResponse = function (eventType, args) { -} - -export const bidWon = function (eventType, args) { - let relevantBidsData = []; - let responses = auctionTracker[args.auctionId][args.adUnitCode].res; - let nonResponses = auctionTracker[args.auctionId][args.adUnitCode].nob; - let bidResponders = Object.keys(responses); - let noBidResponders = Object.keys(nonResponses); - - let bidResponsesRaw = auctionTracker[args.auctionId][args.adUnitCode].res - let winningBid = {}; - let winningBidData = auctionEndStorage.bidsReceived.filter( - bid => bid.bidderCode === args.bidderCode && bid.adUnitCode === args.adUnitCode - )[0] - - winningBid.adTagId = args.adUnitCode; - winningBid.bid = true; - winningBid.language = window.navigator.language || ''; - winningBid.userAgent = window.navigator.userAgent || ''; - if (navigator.userAgentData && navigator.userAgentData.platform) { - winningBid.platform = navigator.userAgentData.platform; - } else { - winningBid.platform = navigator.platform; - } - winningBid.timeZone = new Date().getTimezoneOffset() / 60; - winningBid.width = winningBidData.width ? winningBidData.width : undefined; - winningBid.height = winningBidData.height ? winningBidData.height : undefined; - winningBid.screenWidth = screen.width; - winningBid.screenHeight = screen.height; - winningBid.size = winningBid.width && winningBid.height ? `${winningBidData.width}x${winningBidData.height}` : ''; - winningBid.win = true; - - winningBid.cost = args.cpm / 1000; - winningBid.currency = args.currency; - winningBid.delay = args.timeToRespond; - winningBid.domain = auctionTracker.domain; - winningBid.ssp = args.bidder; - - relevantBidsData.push(winningBid); - for (let bidder of bidResponders) { - if (bidResponsesRaw[bidder].ssp !== winningBid.ssp) { - relevantBidsData.push(bidResponsesRaw[bidder]); - } - } - - for (let bidder of noBidResponders) { - relevantBidsData.push(nonResponses[bidder]); - } - - // Send the JSON-stringified array to server - let payload = JSON.stringify(relevantBidsData); - let platformUrlMatch = platformURL.match(/.+(?=protocol\.php)/) - let fullPlatformURL = (platformUrlMatch ? platformUrlMatch[0] : platformURL) + 'data.php?type=pbstats'; - - ajax(fullPlatformURL, null, payload); - if (bidderAnalyticsDomain && publisherId) { - let optionalForwardSlash = bidderAnalyticsDomain.match(/\/$/) ? '' : '/'; - let bidderAnalyticsURL = `${bidderAnalyticsDomain}${optionalForwardSlash}protocol.php?action=prebid_impression&version=${VERSION}` - - let bidderPayload = JSON.stringify( - { - platform: publisherId, - data: relevantBidsData - } - ); - ajax(bidderAnalyticsURL, null, bidderPayload); - } -} - -export const bidTimeout = function (eventType, args) { - bidTimeouts = args; -} - -export const auctionEnd = function (eventType, args) { - auctionEndStorage = args; - var adUnitsHaveBids = {}; - var adUnitsHaveAdhashFallback = {}; - var adUnitsHaveOtherFallbacks = {}; - for (let adUnit of args.adUnitCodes) { - adUnitsHaveBids[adUnit] = false; - adUnitsHaveAdhashFallback[adUnit] = false; - adUnitsHaveOtherFallbacks[adUnit] = false; - } - - // adding domain here: - if (!auctionTracker.domain) { - try { - auctionTracker.domain = window.top.location.host; - } catch (e) { - auctionTracker.domain = ''; - } - } - - // Populate Request info - args.bidderRequests.forEach(req => { - for (var bid of req.bids) { - auctionTracker[req.auctionId][bid.adUnitCode].req[req.bidderCode] = { - ssp: req.bidderCode, - domain: auctionTracker.domain, - delay: null, - bid: false, - win: false, - timeout: false, - cost: null - } - } - }) - - // Populate Response info - args.bidsReceived.forEach(res => { - var unitAuction = auctionTracker[res.auctionId][res.adUnitCode]; - unitAuction.res[res.bidderCode] = { - ssp: res.bidderCode, - domain: auctionTracker.domain, - delay: res.timeToRespond, - bid: true, - win: false, - timeout: false, - cost: res.cpm / 1000, - currency: res.currency - } - if (res.width && res.height) { - unitAuction.res[res.bidderCode]['width'] = res.width; - unitAuction.res[res.bidderCode]['height'] = res.height; - } - adUnitsHaveBids[res.adUnitCode] = true; - }) - - args.noBids.forEach(res => { - var unitAuction = auctionTracker[res.auctionId][res.adUnitCode]; - - var nobObj = unitAuction.nob; - nobObj[res.bidder] = { - ssp: res.bidder, - domain: auctionTracker.domain, - delay: null, - bid: false, - win: false, - timeout: false, - cost: 0.0, - } - if (res.bidder === 'adhash') { - adUnitsHaveAdhashFallback[res.adUnitCode] = true; - } else { - adUnitsHaveOtherFallbacks[res.adUnitCode] = true; - } - }) - - bidTimeouts.forEach(req => { - var unitAuction = auctionTracker[req.auctionId][req.adUnitCode]; - var noBidObject = unitAuction.nob; - if (!noBidObject[req.bidder]) { - noBidObject[req.bidder] = { - ssp: req.bidder, - domain: auctionTracker.domain, - bid: false, - win: false, - timeout: true, - cost: 0.0, - } - } else { - noBidObject[req.bidder].timeout = true; - } - }) - - // Send fallback data for each ad unit - for (let adUnit of args.adUnitCodes) { - if (adUnitsHaveBids[adUnit]) { - continue; - } - let fallbackData = {}; - fallbackData.adTagId = adUnit; - fallbackData.pageURL = window.location.href; - if (navigator.userAgentData && navigator.userAgentData.platform) { - fallbackData.platform = navigator.userAgentData.platform; - } else { - fallbackData.platform = navigator.platform; - } - fallbackData.language = window.navigator.language || ''; - fallbackData.userAgent = window.navigator.userAgent || ''; - fallbackData.screenWidth = screen.width; - fallbackData.screenHeight = screen.height; - fallbackData.timeZone = new Date().getTimezoneOffset() / 60; - fallbackData.hasAdhashFallback = adUnitsHaveAdhashFallback[adUnit]; - fallbackData.hasOtherFallbacks = adUnitsHaveOtherFallbacks[adUnit]; - fallbackSaver = fallbackData; - - let payload = JSON.stringify(fallbackData); - let platformUrlMatch = platformURL.match(/.+(?=protocol\.php)/); - let fullPlatformURL = (platformUrlMatch ? platformUrlMatch[0] : platformURL) + 'data.php?type=pbfallback'; - - ajax(fullPlatformURL, null, payload); - } -} - -export const noBid = function (eventType, args) { - var auctionId = args.auctionId; - var adUnitCode = args.adUnitCode; - var bidder = args.bidder; - auctionTracker[auctionId][adUnitCode].nob[bidder] = { - bid: false, - cost: 0, - domain: auctionTracker.domain, - ssp: bidder, - timeout: false, - win: false - } -} - -const { - EVENTS: { - AUCTION_INIT, - BID_REQUESTED, - BID_TIMEOUT, - BID_RESPONSE, - BID_WON, - AUCTION_END, - NO_BID - } -} = CONSTANTS; - -let adhashAdapter = Object.assign(adapter({ defaultUrl, analyticsType }), { - track({ eventType, args }) { - switch (eventType) { - case AUCTION_INIT: - auctionInit(eventType, args); - break; - case BID_REQUESTED: - bidRequested(eventType, args); - break; - case BID_RESPONSE: - bidResponse(eventType, args); - break; - case BID_WON: - bidWon(eventType, args); // Send the data here. - break; - case BID_TIMEOUT: - bidTimeout(eventType, args); - break; - case AUCTION_END: - auctionEnd(eventType, args); - break; - case NO_BID: - noBid(eventType, args); - break; - default: - break; - } - } -}); -adhashAdapter.context = {}; - -adhashAdapter.originEnableAnalytics = adhashAdapter.enableAnalytics; -adhashAdapter.enableAnalytics = (config) => { - adhashAdapter.initOptions = config.options; - platformURL = adhashAdapter.initOptions.platformURL; - bidderAnalyticsDomain = adhashAdapter.initOptions.bidderURL; - publisherId = adhashAdapter.initOptions.publisherId; - - adhashAdapter.originEnableAnalytics(config); -}; - -adapterManager.registerAnalyticsAdapter({ - adapter: adhashAdapter, - code: 'adhash' -}); - -// Functions needed for unit testing -export function getAuctionTracker () { - return auctionTracker; -} - -export function getTimeouts() { - return bidTimeouts; -} - -export function getSavedFallbackData() { - return fallbackSaver; -} - -export function clearSavedFallbackData() { - fallbackSaver = {}; -} - -export default adhashAdapter; diff --git a/test/spec/modules/adhashAnalyticsAdapter_spec.js b/test/spec/modules/adhashAnalyticsAdapter_spec.js deleted file mode 100644 index ac6dffde456..00000000000 --- a/test/spec/modules/adhashAnalyticsAdapter_spec.js +++ /dev/null @@ -1,436 +0,0 @@ -import { expect } from 'chai'; -import adhashAdapter from 'modules/adhashAnalyticsAdapter.js'; -import { getAuctionTracker, getTimeouts, getSavedFallbackData,clearSavedFallbackData } from 'modules/adhashAnalyticsAdapter.js'; -import CONSTANTS from 'src/constants.json'; -import * as events from 'src/events.js'; - -const { - EVENTS: { - AUCTION_INIT, - BID_REQUESTED, - BID_TIMEOUT, - BID_RESPONSE, - BID_WON, - AUCTION_END, - NO_BID - } -} = CONSTANTS; - -const AD_UNIT_CODE = 'test-ad-unit'; -const AUCTION_ID = 'test-auction-id'; -const CPM_WIN = 5; -const CPM_LOSE = 4; -const CURRENT_TIME = 1663318800000; -const SLOT_LOAD_WAIT_TIME = 10; - -const ANALYTICS_CONFIG = { - publisherId: '0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb', - platformURL: 'https://adhash.com/p/struma/', - orgId: 'test-org-id', - publisherAccountId: 123, - publisherPlatformId: 'test-platform-id', - configId: 'my_config', - optimizerConfig: 'my my optimizer', - sample: 1.0, - payloadWaitTime: SLOT_LOAD_WAIT_TIME, - payloadWaitTimePadding: SLOT_LOAD_WAIT_TIME -}; - -const auctionInitArgs = { - auctionId: AUCTION_ID, - timestamp: CURRENT_TIME, - timeout: 3000, - adUnitCodes: [AD_UNIT_CODE], -}; - -const bidRequestedAdhashArgs = { - auctionId: AUCTION_ID, - bidderCode: 'adhash', - auctionStart: CURRENT_TIME, - timeout: 2000, - bids: [ - { - adUnitCode: AD_UNIT_CODE, - bidId: 'adhash-request-id', - params: { unit: 'adhash-ad-unit-id' }, - } - ], - start: CURRENT_TIME + 10 -}; - -const bidRequestedOpenXArgs = { - auctionId: AUCTION_ID, - bidderCode: 'openx', - auctionStart: CURRENT_TIME, - timeout: 1000, - bids: [ - { - adUnitCode: AD_UNIT_CODE, - bidId: 'openx-request-id', - params: { unit: 'openx-ad-unit-id' }, - } - ], - start: CURRENT_TIME + 20 -}; - -const bidResponseAdhashArgs = { - adUnitCode: AD_UNIT_CODE, - bidderCode: 'adhash', - cpm: CPM_WIN, - netRevenue: true, - requestId: 'adhash-request-id', - mediaType: 'banner', - width: 300, - height: 250, - adId: 'adhash-ad-id', - auctionId: AUCTION_ID, - creativeId: 'adhash-creative-id', - currency: 'BG', - timeToRespond: 100, - responseTimestamp: CURRENT_TIME + 30, - ts: 'test-adhash-ts' -}; - -const bidResponseOpenXArgs = { - adUnitCode: AD_UNIT_CODE, - bidderCode: 'openx', - cpm: CPM_LOSE, - netRevenue: true, - requestId: 'openx-request-id', - mediaType: 'banner', - width: 300, - height: 250, - adId: 'openx-ad-id', - auctionId: AUCTION_ID, - creativeId: 'openx-creative-id', - currency: 'BG', - timeToRespond: 100, - responseTimestamp: CURRENT_TIME + 40, - ts: 'test-openx-ts' -}; - -const bidRequestedRubiconArgs = { - auctionId: AUCTION_ID, - bidderCode: 'rubicon', - auctionStart: CURRENT_TIME, - timeout: 1000, - bids: [], - start: CURRENT_TIME + 50 -}; - -const noBidsRubiconArgs = { - adUnitCode: AD_UNIT_CODE, - auctionId: AUCTION_ID, - bidder: 'rubicon' -} - -const bidTimeoutRubiconArgs = { - adUnitCode: AD_UNIT_CODE, - auctionId: AUCTION_ID, - bidder: 'rubicon' -} - -const bidWonAdhashArgs = { - adUnitCode: AD_UNIT_CODE, - auctionId: AUCTION_ID, - bidderCode: 'adhash', - requestId: 'adhash-request-id', - adId: 'adhash-ad-id' -}; - -const auctionEndArgs = { - auctionId: AUCTION_ID, - timestamp: CURRENT_TIME, - auctionEnd: CURRENT_TIME + 100, - timeout: 3000, - adUnitCodes: [AD_UNIT_CODE], -}; - -describe('adhashAnalyticsAdapter', function () { - let clock; - - beforeEach(function() { - sinon.stub(events, 'getEvents').returns([]); - clock = sinon.useFakeTimers(CURRENT_TIME); - }); - - afterEach(function() { - events.getEvents.restore(); - clock.restore(); - }); - - describe('auctionInit', function () { - adhashAdapter.enableAnalytics({options: ANALYTICS_CONFIG}); - - let clock = sinon.useFakeTimers(CURRENT_TIME); - clock.tick(SLOT_LOAD_WAIT_TIME * 2); - - it('should initialize the auction tracker with empty data for each ad unit', function() { - simulateAuction([ - [AUCTION_INIT, auctionInitArgs] - ]); - let auctionTracker = getAuctionTracker(); - var unitIsInitialized = auctionTracker[AUCTION_ID] != undefined && auctionTracker[AUCTION_ID][[auctionInitArgs.adUnitCodes[0]]] != undefined; - expect(unitIsInitialized).is.true; - }); - - adhashAdapter.disableAnalytics(); - }); - - describe('auctionEnd with fallback', function () { - let clock = sinon.useFakeTimers(CURRENT_TIME); - - beforeEach(function () { - adhashAdapter.enableAnalytics({options: ANALYTICS_CONFIG}); - - simulateAuction([ - [AUCTION_INIT, auctionInitArgs], - [BID_REQUESTED, bidRequestedRubiconArgs], - [NO_BID, noBidsRubiconArgs], - [AUCTION_END, auctionEndArgs] - ]); - - clock.tick(SLOT_LOAD_WAIT_TIME * 2); - }); - - afterEach(function () { - adhashAdapter.disableAnalytics(); - }); - - it('should have fallback data', function () { - let fallbackData = getSavedFallbackData(); - expect(fallbackData).to.contain({adTagId: AD_UNIT_CODE}); - clearSavedFallbackData(); - }); - }); - - describe('auctionEnd', function () { - let adhashBidResponse; - let openxBidResponse; - let auctionTracker; - - let clock = sinon.useFakeTimers(CURRENT_TIME); - beforeEach(function () { - adhashAdapter.enableAnalytics({options: ANALYTICS_CONFIG}); - - simulateAuction([ - [AUCTION_INIT, auctionInitArgs], - [BID_REQUESTED, bidRequestedAdhashArgs], - [BID_REQUESTED, bidRequestedOpenXArgs], - [BID_RESPONSE, bidResponseAdhashArgs], - [BID_RESPONSE, bidResponseOpenXArgs], - [BID_REQUESTED, bidRequestedRubiconArgs], - [AUCTION_END, auctionEndArgs] - ]); - - clock.tick(SLOT_LOAD_WAIT_TIME * 2); - - auctionTracker = getAuctionTracker(); - adhashBidResponse = auctionTracker[AUCTION_ID][AD_UNIT_CODE]['res']['adhash']; - openxBidResponse = auctionTracker[AUCTION_ID][AD_UNIT_CODE]['res']['openx']; - }); - - afterEach(function () { - adhashAdapter.disableAnalytics(); - }); - - it('should have a cost for a single impression for every bid based on the CPM', function () { - let adhashCost = adhashBidResponse.cost ? adhashBidResponse.cost : 0; - let openxCost = openxBidResponse.cost ? openxBidResponse.cost : 0; - let adhashCostDifference = Math.abs(adhashCost - (CPM_WIN / 1000)); - let openxCostDifference = Math.abs(openxCost - (CPM_LOSE / 1000)); - expect(adhashBidResponse.cost).is.not.undefined; - expect(openxBidResponse.cost).is.not.undefined; - expect(adhashCostDifference).lt(Number.EPSILON); - expect(openxCostDifference).lt(Number.EPSILON); - }); - - it('should track the currency', function () { - expect(adhashBidResponse.currency).to.equal(bidResponseAdhashArgs.currency); - expect(openxBidResponse.currency).to.equal(bidResponseOpenXArgs.currency); - }); - - it('should track the bid\'s latency', function () { - expect(adhashBidResponse.delay).to.equal(bidResponseAdhashArgs.timeToRespond); - expect(openxBidResponse.delay).to.equal(bidResponseOpenXArgs.timeToRespond); - }); - - it('should not have any bid winners', function () { - expect(adhashBidResponse.win).to.equal(false); - expect(openxBidResponse.win).to.equal(false); - }); - }); - - describe('bidWon', function () { - let adhashBidResponse; - let openxBidResponse; - let auctionTracker; - - let clock = sinon.useFakeTimers(CURRENT_TIME); - beforeEach(function () { - adhashAdapter.enableAnalytics({options: ANALYTICS_CONFIG}); - - simulateAuction([ - [AUCTION_INIT, auctionInitArgs], - [BID_REQUESTED, bidRequestedAdhashArgs], - [BID_REQUESTED, bidRequestedOpenXArgs], - [BID_RESPONSE, bidResponseAdhashArgs], - [BID_RESPONSE, bidResponseOpenXArgs], - [AUCTION_END, auctionEndArgs], - [BID_WON, bidWonAdhashArgs] - ]); - - clock.tick(SLOT_LOAD_WAIT_TIME * 2); - - auctionTracker = getAuctionTracker(); - adhashBidResponse = auctionTracker[AUCTION_ID][AD_UNIT_CODE]['res']['adhash']; - openxBidResponse = auctionTracker[AUCTION_ID][AD_UNIT_CODE]['res']['openx']; - }); - - afterEach(function () { - adhashAdapter.disableAnalytics(); - }); - - it('should have a winning bidder that was marked as having a bid', function () { - expect(adhashBidResponse).to.contain({bid: true}); - }); - - it('should mark the rest of the bidders as the losers', function () { - expect(openxBidResponse).to.contain({win: false}); - }); - }); - - describe('noBid', function () { - let auctionTracker; - let clock = sinon.useFakeTimers(CURRENT_TIME); - - beforeEach(function () { - adhashAdapter.enableAnalytics({options: ANALYTICS_CONFIG}); - - simulateAuction([ - [AUCTION_INIT, auctionInitArgs], - [BID_REQUESTED, bidRequestedOpenXArgs], - [BID_RESPONSE, bidResponseOpenXArgs], - [BID_REQUESTED, bidRequestedRubiconArgs], - [NO_BID, noBidsRubiconArgs], - [AUCTION_END, auctionEndArgs] - ]); - - clock.tick(SLOT_LOAD_WAIT_TIME * 2); - auctionTracker = getAuctionTracker(); - }); - - afterEach(function () { - adhashAdapter.disableAnalytics(); - }); - - it('should add the responses with no bids to the auction tracker', function() { - expect(auctionTracker[AUCTION_ID][AD_UNIT_CODE].nob['rubicon']).is.not.undefined; - }); - - it('should not add the responses with bids to the responses with no bids', function() { - expect(auctionTracker[AUCTION_ID][AD_UNIT_CODE].nob['openx']).is.undefined; - }); - - it('should mark the responses with no bids as not winning', function() { - expect(auctionTracker[AUCTION_ID][AD_UNIT_CODE].nob['rubicon']['bid']).is.false; - expect(auctionTracker[AUCTION_ID][AD_UNIT_CODE].nob['rubicon']['win']).is.false; - }); - }); - - describe('bidTimeout', function () { - let clock = sinon.useFakeTimers(CURRENT_TIME); - - beforeEach(function () { - adhashAdapter.enableAnalytics({options: ANALYTICS_CONFIG}); - }); - - afterEach(function () { - adhashAdapter.disableAnalytics(); - }); - - it('should add the bid timeout data to the auction tracker', function () { - simulateAuction([ - [AUCTION_INIT, auctionInitArgs], - [BID_REQUESTED, bidRequestedRubiconArgs], - [BID_TIMEOUT, [bidTimeoutRubiconArgs]], - [AUCTION_END, auctionEndArgs] - ]); - - clock.tick(SLOT_LOAD_WAIT_TIME * 2); - - let timeoutData = getTimeouts(); - for (let singleTimeout of timeoutData) { - expect(singleTimeout).to.contain({adUnitCode: AD_UNIT_CODE}); - } - }); - - it('should mark the bid with timeout as such when it hasn\'t been added to the bidders with no bids', function () { - simulateAuction([ - [AUCTION_INIT, auctionInitArgs], - [BID_REQUESTED, bidRequestedRubiconArgs], - [BID_TIMEOUT, [bidTimeoutRubiconArgs]], - [AUCTION_END, auctionEndArgs] - ]); - - clock.tick(SLOT_LOAD_WAIT_TIME * 2); - - let auctionTracker = getAuctionTracker(); - expect(auctionTracker[AUCTION_ID][AD_UNIT_CODE]['nob'][bidTimeoutRubiconArgs.bidder]).to.contain({timeout: true}); - }); - - it('should mark the bid with timeout as such when it has already been added to the bidders with no bids', function () { - simulateAuction([ - [AUCTION_INIT, auctionInitArgs], - [BID_REQUESTED, bidRequestedRubiconArgs], - [NO_BID, noBidsRubiconArgs], - [BID_TIMEOUT, [bidTimeoutRubiconArgs]], - [AUCTION_END, auctionEndArgs] - ]); - - clock.tick(SLOT_LOAD_WAIT_TIME * 2); - - let auctionTracker = getAuctionTracker(); - expect(auctionTracker[AUCTION_ID][AD_UNIT_CODE]['nob'][bidTimeoutRubiconArgs.bidder]).to.contain({timeout: true}); - }); - }); - - function simulateAuction(events) { - let highestBid; - let bidRequests = []; - let bidResponses = []; - let noBids = []; - let allArgs = {}; - - for (let event of events) { - const [eventType, args] = event; - if (eventType == BID_RESPONSE) { - highestBid = highestBid || args; - if (highestBid.cpm < args.cpm) { - highestBid = args; - } - bidResponses.push(args); - allArgs['bidsReceived'] = bidResponses; - } else if (eventType == BID_REQUESTED) { - bidRequests.push(args); - allArgs['bidderRequests'] = bidRequests; - } else if (eventType == NO_BID) { - noBids.push(args); - adhashAdapter.track({ eventType, args }); - continue; - } else if (eventType == BID_WON || eventType == BID_TIMEOUT) { - adhashAdapter.track({ eventType, args }); - continue; - } else if (eventType == AUCTION_INIT) { - allArgs = args; - } else if (eventType == AUCTION_END) { - allArgs['noBids'] = noBids; - if (!allArgs['bidsReceived']) { - allArgs['bidsReceived'] = []; - } - } - - adhashAdapter.track({ eventType, args: allArgs }); - }; - } -}); From 65a0d2624b1f5eade49efce25b1b51b855610e00 Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Mon, 28 Nov 2022 10:05:09 +0200 Subject: [PATCH 18/23] Fix for regexp match --- modules/adhashBidAdapter.js | 14 +++++++------- test/spec/modules/adhashBidAdapter_spec.js | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 30f70a5b43c..6504b4634ad 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -72,8 +72,8 @@ function brandSafety(badWords, maxScore) { return { rule, occurances: wordsToMatch.filter(element => element.startsWith(decodedWord)).length }; } else if (rule === 'ends' && wordsToMatch && wordsToMatch.some(word => word.endsWith(decodedWord))) { return { rule, occurances: wordsToMatch.filter(element => element.endsWith(decodedWord)).length }; - } else if (rule === 'regexp' && wordsToMatch && wordsToMatch.includes(decodedWord)) { - return { rule, occurances: wordsToMatch.filter(element => element === decodedWord).length }; + } else if (rule === 'regexp' && wordsToMatch && wordsToMatch.some(element => element.match(new RegExp(decodedWord, 'i')))) { + return { rule, occurances: wordsToMatch.filter(element => element.match(new RegExp(decodedWord, 'i'))).length }; } return false; }; @@ -157,18 +157,18 @@ export const spec = { } catch (e) { referrer = window.location.href; } - var body = document.body; - var html = document.documentElement; - var pageHeight = Math.max( + const body = document.body; + const html = document.documentElement; + const pageHeight = Math.max( body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight ); - var pageWidth = Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth); + const pageWidth = Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth); - for (const i = 0; i < validBidRequests.length; i++) { + for (let i = 0; i < validBidRequests.length; i++) { const bidderURL = validBidRequests[i].params.bidderURL || 'https://bidder.adhash.com'; const url = `${bidderURL}/rtb?version=${VERSION}&prebid=true`; const index = Math.floor(Math.random() * validBidRequests[i].sizes.length); diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js index 57bfe5e76a3..5bad38ef35c 100644 --- a/test/spec/modules/adhashBidAdapter_spec.js +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -127,8 +127,8 @@ describe('adhashBidAdapter', function () { creatives: [{ costEUR: 1.234 }], advertiserDomains: 'adhash.com', badWords: [ - ['onqjbeq1', 'full', 1], - ['onqjbeq2', 'partial', 1], + ['onqjbeq', 'full', 1], + ['onqjbeqo', 'partial', 1], ['tbbqjbeq', 'full', -1], ['fgnegf', 'starts', 1], ['raqf', 'ends', 1], @@ -161,7 +161,7 @@ describe('adhashBidAdapter', function () { it('should return empty array when there are bad words (full)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text badWord1 badWord1 example badWord1 text' + ' word'.repeat(993); + return 'example text badword badword example badword text' + ' word'.repeat(993); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); @@ -175,7 +175,7 @@ describe('adhashBidAdapter', function () { it('should return empty array when there are bad words (partial)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text partialBadWord2 badword2 example BadWord2text' + ' word'.repeat(994); + return 'example text partialbadwordb badwordb example badwordbtext' + ' word'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); @@ -217,28 +217,28 @@ describe('adhashBidAdapter', function () { it('should return non-empty array when there are not enough bad words (full)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text badWord1 badWord1 example text' + ' word'.repeat(994); + return 'example text badword badword example text' + ' word'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return non-empty array when there are not enough bad words (partial)', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text partialBadWord2 example' + ' word'.repeat(996); + return 'example text partialbadwordb example' + ' word'.repeat(996); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return non-empty array when there are no-bad word matches', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text partialBadWord1 example text' + ' word'.repeat(995); + return 'example text partialbadword example text' + ' word'.repeat(995); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return non-empty array when there are bad words and good words', function () { bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { - return 'example text badWord1 badWord1 example badWord1 goodWord goodWord ' + ' word'.repeat(992); + return 'example text badword badword example badword goodWord goodWord ' + ' word'.repeat(992); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); From e27cbe4db76be7ac921778d348da1cf0e0c6b7ba Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Mon, 28 Nov 2022 10:15:01 +0200 Subject: [PATCH 19/23] Version change --- modules/adhashBidAdapter.js | 2 +- test/spec/modules/adhashBidAdapter_spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 6504b4634ad..3db07a399cc 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -3,7 +3,7 @@ import { getStorageManager } from '../src/storageManager.js'; import { includes } from '../src/polyfill.js'; import { BANNER } from '../src/mediaTypes.js'; -const VERSION = '3.0'; +const VERSION = '3.2'; const BAD_WORD_STEP = 0.1; const BAD_WORD_MIN = 0.2; const ADHASH_BIDDER_CODE = 'adhash'; diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js index 5bad38ef35c..84ada9eb7e2 100644 --- a/test/spec/modules/adhashBidAdapter_spec.js +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -77,7 +77,7 @@ describe('adhashBidAdapter', function () { ); expect(result.length).to.equal(1); expect(result[0].method).to.equal('POST'); - expect(result[0].url).to.equal('https://bidder.adhash.com/rtb?version=3.0&prebid=true&publisher=0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb'); + expect(result[0].url).to.equal('https://bidder.adhash.com/rtb?version=3.2&prebid=true&publisher=0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb'); expect(result[0].bidRequest).to.equal(bidRequest); expect(result[0].data).to.have.property('timezone'); expect(result[0].data).to.have.property('location'); @@ -93,7 +93,7 @@ describe('adhashBidAdapter', function () { const result = spec.buildRequests([ bidRequest ], { gdprConsent: { gdprApplies: true, consentString: 'example' } }); expect(result.length).to.equal(1); expect(result[0].method).to.equal('POST'); - expect(result[0].url).to.equal('https://bidder.adhash.com/rtb?version=3.0&prebid=true&publisher=0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb'); + expect(result[0].url).to.equal('https://bidder.adhash.com/rtb?version=3.2&prebid=true&publisher=0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb'); expect(result[0].bidRequest).to.equal(bidRequest); expect(result[0].data).to.have.property('timezone'); expect(result[0].data).to.have.property('location'); From d68efac00358a88cb333423fc5d408cafc7d4957 Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Wed, 7 Dec 2022 09:38:18 +0200 Subject: [PATCH 20/23] MINOR Code review changes --- modules/adhashBidAdapter.js | 13 ++++--------- test/spec/modules/adhashBidAdapter_spec.js | 8 +++++++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 3db07a399cc..33a85a81525 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -134,13 +134,14 @@ export const spec = { isBidRequestValid: (bid) => { try { - const { publisherId, platformURL } = bid.params; + const { publisherId, platformURL, bidderURL } = bid.params; return ( includes(Object.keys(bid.mediaTypes), BANNER) && typeof publisherId === 'string' && publisherId.length === 42 && typeof platformURL === 'string' && - platformURL.length >= 13 + platformURL.length >= 13 && + (!bidderURL || bidderURL.indexOf('https://') === 0) ); } catch (error) { return false; @@ -151,12 +152,6 @@ export const spec = { const storage = getStorageManager({ bidderCode: ADHASH_BIDDER_CODE }); const { gdprConsent } = bidderRequest; const bidRequests = []; - let referrer = ''; - try { - referrer = window.top.location.href; - } catch (e) { - referrer = window.location.href; - } const body = document.body; const html = document.documentElement; const pageHeight = Math.max( @@ -198,7 +193,7 @@ export const spec = { bidRequest: validBidRequests[i], data: { timezone: new Date().getTimezoneOffset() / 60, - location: referrer, + location: bidderRequest.refererInfo ? bidderRequest.refererInfo.topmostLocation : '', publisherId: validBidRequests[i].params.publisherId, size: { screenWidth: window.screen.width, diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js index 84ada9eb7e2..4ea525c59d5 100644 --- a/test/spec/modules/adhashBidAdapter_spec.js +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -60,6 +60,12 @@ describe('adhashBidAdapter', function () { bid.params.platformURL = 'https://'; expect(spec.isBidRequestValid(bid)).to.equal(false); }); + + it('should return false when bidderURL is present but not https://', function () { + const bid = { ...validBid }; + bid.params.bidderURL = 'http://example.com/'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); }); describe('buildRequests', function () { @@ -73,7 +79,7 @@ describe('adhashBidAdapter', function () { it('should build the request correctly', function () { const result = spec.buildRequests( [ bidRequest ], - { gdprConsent: { gdprApplies: true, consentString: 'example' }, refererInfo: { referer: 'http://example.com/' } } + { gdprConsent: { gdprApplies: true, consentString: 'example' }, refererInfo: { topmostLocation: 'https://example.com/path.html' } } ); expect(result.length).to.equal(1); expect(result[0].method).to.equal('POST'); From 78dea30b0401d8ba5a52cdff7b428f8dcea9f8ac Mon Sep 17 00:00:00 2001 From: NikolayMGeorgiev Date: Thu, 27 Apr 2023 09:41:34 +0300 Subject: [PATCH 21/23] GEN-1153 Adding support for preroll ads --- modules/adhashBidAdapter.js | 41 ++++++++++++++-------- test/spec/modules/adhashBidAdapter_spec.js | 12 ++++++- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 33a85a81525..8262c9a6ec5 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -1,7 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { includes } from '../src/polyfill.js'; -import { BANNER } from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; const VERSION = '3.2'; const BAD_WORD_STEP = 0.1; @@ -130,13 +130,13 @@ function brandSafety(badWords, maxScore) { export const spec = { code: ADHASH_BIDDER_CODE, - supportedMediaTypes: [ BANNER ], + supportedMediaTypes: [ BANNER, VIDEO ], isBidRequestValid: (bid) => { try { const { publisherId, platformURL, bidderURL } = bid.params; return ( - includes(Object.keys(bid.mediaTypes), BANNER) && + (includes(Object.keys(bid.mediaTypes), BANNER) || includes(Object.keys(bid.mediaTypes), VIDEO)) && typeof publisherId === 'string' && publisherId.length === 42 && typeof platformURL === 'string' && @@ -168,7 +168,14 @@ export const spec = { const url = `${bidderURL}/rtb?version=${VERSION}&prebid=true`; const index = Math.floor(Math.random() * validBidRequests[i].sizes.length); const size = validBidRequests[i].sizes[index].join('x'); - + const creativeData = includes(Object.keys(validBidRequests[i].mediaTypes), VIDEO) ? { + size: 'preroll', + position: validBidRequests[i].adUnitCode, + playerSize: size + } : { + size: size, + position: validBidRequests[i].adUnitCode + }; let recentAds = []; if (storage.localStorageIsEnabled()) { const prefix = validBidRequests[i].params.prefix || 'adHash'; @@ -204,10 +211,7 @@ export const spec = { language: window.navigator.language, userAgent: window.navigator.userAgent }, - creatives: [{ - size: size, - position: validBidRequests[i].adUnitCode - }], + creatives: [creativeData], blockedCreatives: [], currentTimestamp: (new Date().getTime() / 1000) | 0, recentAds: recentAds, @@ -229,7 +233,6 @@ export const spec = { interpretResponse: (serverResponse, request) => { const responseBody = serverResponse ? serverResponse.body : {}; - if ( !responseBody.creatives || responseBody.creatives.length === 0 || @@ -247,12 +250,9 @@ export const spec = { const bidderResponse = JSON.stringify({ responseText: JSON.stringify(responseBody) }); const requestData = JSON.stringify(request.data); - return [{ + var response = { requestId: request.bidRequest.bidId, cpm: responseBody.creatives[0].costEUR, - ad: - `
${globalScript} - `, width: request.bidRequest.sizes[0][0], height: request.bidRequest.sizes[0][1], creativeId: request.bidRequest.adUnitCode, @@ -262,7 +262,20 @@ export const spec = { meta: { advertiserDomains: responseBody.advertiserDomains ? [responseBody.advertiserDomains] : [] } - }]; + }; + if (typeof request == 'object' && typeof request.bidRequest == 'object' && typeof request.bidRequest.mediaTypes == 'object' && includes(Object.keys(request.bidRequest.mediaTypes), BANNER)) { + response = Object.assign({ + ad: + `
${globalScript} + ` + }, response); + } else if (includes(Object.keys(request.bidRequest.mediaTypes), VIDEO)) { + response = Object.assign({ + vastUrl: responseBody.creatives[0].vastURL, + mediaType: VIDEO + }, response); + } + return [response]; } }; diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js index 4ea525c59d5..2477ce03dbc 100644 --- a/test/spec/modules/adhashBidAdapter_spec.js +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -74,7 +74,12 @@ describe('adhashBidAdapter', function () { publisherId: '0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb' }, sizes: [[300, 250]], - adUnitCode: 'adUnitCode' + adUnitCode: 'adUnitCode', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } }; it('should build the request correctly', function () { const result = spec.buildRequests( @@ -122,6 +127,11 @@ describe('adhashBidAdapter', function () { sizes: [[300, 250]], params: { platformURL: 'https://adhash.com/p/struma/' + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } } } }; From 340e47353d5c85327f51b74e2d9611729acee020 Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Fri, 28 Apr 2023 09:56:31 +0300 Subject: [PATCH 22/23] MINOR Video unit test added --- test/spec/modules/adhashBidAdapter_spec.js | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js index 2477ce03dbc..2d3458fff79 100644 --- a/test/spec/modules/adhashBidAdapter_spec.js +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -277,5 +277,45 @@ describe('adhashBidAdapter', function () { it('should return empty array when something is not right', function () { expect(spec.interpretResponse(null, request).length).to.equal(0); }); + + it('should interpret the video response correctly', function () { + const result = spec.interpretResponse({ + body: { + creatives: [{ costEUR: 1.234, vastURL: 'https://example.com/vast' }], + advertiserDomains: 'adhash.com' + } + }, { + data: { some: 'data' }, + bidRequest: { + bidId: '12345678901234', + adUnitCode: 'adunit-code', + sizes: [[300, 250]], + params: { + platformURL: 'https://adhash.com/p/struma/' + }, + mediaTypes: { + video: { + context: 'instream', + playerSize: [300, 250], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1 + } + } + } + }); + expect(result.length).to.equal(1); + expect(result[0].requestId).to.equal('12345678901234'); + expect(result[0].cpm).to.equal(1.234); + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(250); + expect(result[0].creativeId).to.equal('adunit-code'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].currency).to.equal('EUR'); + expect(result[0].ttl).to.equal(60); + expect(result[0].meta.advertiserDomains).to.eql(['adhash.com']); + expect(result[0].vastUrl).to.equal('https://example.com/vast'); + }); }); }); From 9ee05e230f369ac7138c9f5ac77045825f55ff02 Mon Sep 17 00:00:00 2001 From: Damyan Stanchev Date: Tue, 9 May 2023 09:17:07 +0300 Subject: [PATCH 23/23] Removing globalScript flag --- modules/adhashBidAdapter.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 8262c9a6ec5..08f9466823b 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -244,9 +244,6 @@ export const spec = { const publisherURL = JSON.stringify(request.bidRequest.params.platformURL); const bidderURL = request.bidRequest.params.bidderURL || 'https://bidder.adhash.com'; const oneTimeId = request.bidRequest.adUnitCode + Math.random().toFixed(16).replace('0.', '.'); - const globalScript = !request.bidRequest.params.globalScript - ? `` - : ''; const bidderResponse = JSON.stringify({ responseText: JSON.stringify(responseBody) }); const requestData = JSON.stringify(request.data); @@ -266,7 +263,8 @@ export const spec = { if (typeof request == 'object' && typeof request.bidRequest == 'object' && typeof request.bidRequest.mediaTypes == 'object' && includes(Object.keys(request.bidRequest.mediaTypes), BANNER)) { response = Object.assign({ ad: - `
${globalScript} + `
+ ` }, response); } else if (includes(Object.keys(request.bidRequest.mediaTypes), VIDEO)) {