Skip to content

Commit

Permalink
AdHash Bid Adapter: brand safety changes (#11617)
Browse files Browse the repository at this point in the history
* AdHash Bidder Adapter: minor changes

We're operating on a com TLD now.
Added publisher in URL for easier routing.

* Implemented brand safety

Implemented brand safety checks

* Fix for GDPR consent

Removing the extra information as request data becomes too big and is sometimes truncated

* Ad fraud prevention formula changed

Ad fraud prevention formula changed to support negative values as well as linear distribution of article length

* 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.

* AdHash brand safety updates

Added support for Cyrillic characters.
Added support for bidderURL parameter.
Fixed score multiplier from 500 to 1000.

* AdHash Analytics adapter

* Support for recent ads

Support for recent ads which gives us the option to do frequency and recency capping.

* Fix for timestamp

* PUB-222

Added logic for measuring the fill rate (fallbacks) for Prebid impressions

* Unit tests for the analytics adapter

Added unit tests for the analytics adapter

* Removed export causing errors

Removed an unneeded export of a const that was causing errors with the analytics adapter

* Added globalScript parameter

* PUB-227

Support for non-latin and non-cyrillic symbols

* 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

* GEN-1025

Sending the needed ad density data to the bidder

* Removing the analytics adaptor

* Fix for regexp match

* Version change

* MINOR

Code review changes

* GEN-1153

Adding support for preroll ads

* MINOR

Video unit test added

* Removing globalScript flag

* Brand safety change

Adding support for compound words as well as combo-patterns.

* Accessing local storage and fixing text selectors

* Adding the options to read and write recent ads from the local storage when enabled.
* Using outerText for whole page text selection.

* Unit tests updated

* Fixing test selector

Using textContent to get the raw text from the body.

* Unit tests fixed

---------

Co-authored-by: NikolayMGeorgiev <[email protected]>
Co-authored-by: Ventsislav Saraminev <[email protected]>
Co-authored-by: Dimitar Kalenderov <[email protected]>
Co-authored-by: NikolaiMGeorgiev <[email protected]>
  • Loading branch information
5 people committed Jun 8, 2024
1 parent dc01e40 commit b3fbd02
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 17 deletions.
20 changes: 18 additions & 2 deletions modules/adhashBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const VERSION = '3.6';
const BAD_WORD_STEP = 0.1;
const BAD_WORD_MIN = 0.2;
const ADHASH_BIDDER_CODE = 'adhash';
const storage = getStorageManager({ bidderCode: ADHASH_BIDDER_CODE });

/**
* Function that checks the page where the ads are being served for brand safety.
Expand Down Expand Up @@ -120,7 +121,7 @@ function brandSafety(badWords, maxScore) {
.replaceAll(/\s\s+/g, ' ')
.toLowerCase()
.trim();
const content = window.top.document.body.innerText.toLowerCase();
const content = window.top.document.body.textContent.toLowerCase();
// \p{L} matches a single unicode code point in the category 'letter'. Matches any kind of letter from any language.
const regexp = new RegExp('[\\p{L}]+', 'gu');
const wordsMatched = content.match(regexp);
Expand Down Expand Up @@ -171,7 +172,6 @@ export const spec = {
},

buildRequests: (validBidRequests, bidderRequest) => {
const storage = getStorageManager({ bidderCode: ADHASH_BIDDER_CODE });
const { gdprConsent } = bidderRequest;
const bidRequests = [];
const body = document.body;
Expand Down Expand Up @@ -199,9 +199,11 @@ export const spec = {
position: validBidRequests[i].adUnitCode
};
let recentAds = [];
let recentAdsPrebid = [];
if (storage.localStorageIsEnabled()) {
const prefix = validBidRequests[i].params.prefix || 'adHash';
recentAds = JSON.parse(storage.getDataFromLocalStorage(prefix + 'recentAds') || '[]');
recentAdsPrebid = JSON.parse(storage.getDataFromLocalStorage(prefix + 'recentAdsPrebid') || '[]');
}

// Needed for the ad density calculation
Expand Down Expand Up @@ -237,6 +239,7 @@ export const spec = {
blockedCreatives: [],
currentTimestamp: (new Date().getTime() / 1000) | 0,
recentAds: recentAds,
recentAdsPrebid: recentAdsPrebid,
GDPRApplies: gdprConsent ? gdprConsent.gdprApplies : null,
GDPR: gdprConsent ? gdprConsent.consentString : null,
servedAdsCount: window.adsCount,
Expand All @@ -263,6 +266,19 @@ export const spec = {
return [];
}

if (storage.localStorageIsEnabled()) {
const prefix = request.bidRequest.params.prefix || 'adHash';
let recentAdsPrebid = JSON.parse(storage.getDataFromLocalStorage(prefix + 'recentAdsPrebid') || '[]');
recentAdsPrebid.push([
(new Date().getTime() / 1000) | 0,
responseBody.creatives[0].advertiserId,
responseBody.creatives[0].budgetId,
responseBody.creatives[0].expectedHashes.length ? responseBody.creatives[0].expectedHashes[0] : '',
]);
let recentAdsPrebidFinal = JSON.stringify(recentAdsPrebid.slice(-100));
storage.setDataInLocalStorage(prefix + 'recentAdsPrebid', recentAdsPrebidFinal);
}

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.', '.');
Expand Down
30 changes: 15 additions & 15 deletions test/spec/modules/adhashBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,105 +178,105 @@ 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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() {
return 'example text badword badword example badword 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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() {
return 'example text partialbadwordb badwordb example badwordbtext' + ' word'.repeat(994);
});
expect(spec.interpretResponse(serverResponse, request).length).to.equal(0);
});

it('should return empty array when there are bad words (partial, compound phrase)', function () {
bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() {
return 'example text partialbad wordb bad wordb example bad wordbtext' + ' 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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() {
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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() {
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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').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 (combo)', function () {
bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() {
return 'queen of england dies, the queen dies' + ' word'.repeat(993);
});
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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() {
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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() {
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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() {
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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() {
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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() {
return 'example text badword badword example badword goodWord goodWord ' + ' word'.repeat(992);
});
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() {
bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() {
return null;
});
expect(spec.interpretResponse(serverResponse, request).length).to.equal(1);
Expand Down

0 comments on commit b3fbd02

Please sign in to comment.