From b532636d80f2f4c287573823c78cc061ae98712d Mon Sep 17 00:00:00 2001 From: Tal Avital Date: Wed, 26 Nov 2025 15:07:09 +0200 Subject: [PATCH 01/15] add deferredBilling support using onBidBillable --- modules/taboolaBidAdapter.js | 10 ++- test/spec/modules/taboolaBidAdapter_spec.js | 83 +++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index dda4694a52c..13e42a4eacd 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -114,6 +114,7 @@ const converter = ortbConverter({ bidResponse(buildBidResponse, bid, context) { const bidResponse = buildBidResponse(bid, context); bidResponse.nurl = bid.nurl; + bidResponse.burl = bid.burl; bidResponse.ad = replaceAuctionPrice(bid.adm, bid.price); if (bid.ext && bid.ext.dchain) { deepSetValue(bidResponse, 'meta.dchain', bid.ext.dchain); @@ -212,11 +213,18 @@ export const spec = { return bids; }, onBidWon: (bid) => { - if (bid.nurl) { + if (bid.nurl && !bid.deferBilling) { const resolvedNurl = replaceAuctionPrice(bid.nurl, bid.originalCpm); ajax(resolvedNurl); } }, + onBidBillable: (bid) => { + const billingUrl = bid.burl || bid.nurl; + if (billingUrl) { + const resolvedBillingUrl = replaceAuctionPrice(billingUrl, bid.originalCpm); + ajax(resolvedBillingUrl); + } + }, getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { const syncs = [] const queryParams = []; diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 8219ec3e8e2..9c06d717a04 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -116,6 +116,89 @@ describe('Taboola Adapter', function () { spec.onBidWon(bid); expect(server.requests[0].url).to.equals('http://win.example.com/3.4') }); + + it('should not fire nurl when deferBilling is true', function () { + const nurl = 'http://win.example.com/${AUCTION_PRICE}'; + const bid = { + requestId: 1, + cpm: 2, + originalCpm: 3.4, + creativeId: 1, + ttl: 60, + netRevenue: true, + mediaType: 'banner', + ad: '...', + width: 300, + height: 250, + nurl: nurl, + deferBilling: true + } + spec.onBidWon(bid); + expect(server.requests.length).to.equal(0); + }); + }); + + describe('onBidBillable', function () { + it('onBidBillable exist as a function', () => { + expect(spec.onBidBillable).to.exist.and.to.be.a('function'); + }); + + it('should fire burl when available', function () { + const burl = 'http://billing.example.com/${AUCTION_PRICE}'; + const nurl = 'http://win.example.com/${AUCTION_PRICE}'; + const bid = { + requestId: 1, + cpm: 2, + originalCpm: 3.4, + creativeId: 1, + ttl: 60, + netRevenue: true, + mediaType: 'banner', + ad: '...', + width: 300, + height: 250, + nurl: nurl, + burl: burl + } + spec.onBidBillable(bid); + expect(server.requests[0].url).to.equals('http://billing.example.com/3.4'); + }); + + it('should fall back to nurl when burl is not available', function () { + const nurl = 'http://win.example.com/${AUCTION_PRICE}'; + const bid = { + requestId: 1, + cpm: 2, + originalCpm: 3.4, + creativeId: 1, + ttl: 60, + netRevenue: true, + mediaType: 'banner', + ad: '...', + width: 300, + height: 250, + nurl: nurl + } + spec.onBidBillable(bid); + expect(server.requests[0].url).to.equals('http://win.example.com/3.4'); + }); + + it('should not fire anything when neither burl nor nurl is available', function () { + const bid = { + requestId: 1, + cpm: 2, + originalCpm: 3.4, + creativeId: 1, + ttl: 60, + netRevenue: true, + mediaType: 'banner', + ad: '...', + width: 300, + height: 250 + } + spec.onBidBillable(bid); + expect(server.requests.length).to.equal(0); + }); }); describe('onTimeout', function () { From a0ffb60c1fa4b3ace9c4c7faf5ded651cb52effb Mon Sep 17 00:00:00 2001 From: Tal Avital Date: Thu, 27 Nov 2025 15:00:40 +0200 Subject: [PATCH 02/15] update burl setting --- modules/taboolaBidAdapter.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 13e42a4eacd..efbb81453a6 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -114,7 +114,9 @@ const converter = ortbConverter({ bidResponse(buildBidResponse, bid, context) { const bidResponse = buildBidResponse(bid, context); bidResponse.nurl = bid.nurl; - bidResponse.burl = bid.burl; + if (bid.burl) { + bidResponse.burl = bid.burl; + } bidResponse.ad = replaceAuctionPrice(bid.adm, bid.price); if (bid.ext && bid.ext.dchain) { deepSetValue(bidResponse, 'meta.dchain', bid.ext.dchain); From 823951230d9c167d7fb8c34fa43fab321e18d7d2 Mon Sep 17 00:00:00 2001 From: Tal Avital Date: Thu, 4 Dec 2025 15:59:20 +0200 Subject: [PATCH 03/15] support nurl firing logic --- modules/taboolaBidAdapter.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index efbb81453a6..3e7127f1ab7 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -218,9 +218,13 @@ export const spec = { if (bid.nurl && !bid.deferBilling) { const resolvedNurl = replaceAuctionPrice(bid.nurl, bid.originalCpm); ajax(resolvedNurl); + bid.taboolaBillingFired = true; } }, onBidBillable: (bid) => { + if (bid.taboolaBillingFired) { + return; + } const billingUrl = bid.burl || bid.nurl; if (billingUrl) { const resolvedBillingUrl = replaceAuctionPrice(billingUrl, bid.originalCpm); From f89d2297ca8948f50f5d416a4729cb7b46e39c00 Mon Sep 17 00:00:00 2001 From: Tal Avital Date: Wed, 24 Dec 2025 17:14:45 +0200 Subject: [PATCH 04/15] add extra signals to taboola request --- modules/taboolaBidAdapter.js | 104 +++++- test/spec/modules/taboolaBidAdapter_spec.js | 379 +++++++++++++++++++- 2 files changed, 470 insertions(+), 13 deletions(-) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 3e7127f1ab7..79e3291ac66 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -7,6 +7,8 @@ import {deepSetValue, getWindowSelf, replaceAuctionPrice, isArray, safeJSONParse import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {isWebdriverEnabled} from '../libraries/webdriver/webdriver.js'; +import {getConnectionType} from '../libraries/connectionInfo/connectionUtils.js'; const BIDDER_CODE = 'taboola'; const GVLID = 42; @@ -21,6 +23,7 @@ const TGID_COOKIE_KEY = 't_gid'; const TGID_PT_COOKIE_KEY = 't_pt_gid'; const TBLA_ID_COOKIE_KEY = 'tbla_id'; export const EVENT_ENDPOINT = 'https://beacon.bidder.taboola.com'; +export const refreshState = {}; /** * extract User Id by that order: @@ -95,6 +98,65 @@ export const internal = { } } +export function getRefreshInfo(adUnitCode) { + const now = Date.now(); + const state = refreshState[adUnitCode]; + + if (!state) { + refreshState[adUnitCode] = { + count: 1, + first: now, + last: now + }; + return { + count: 1, + first: now, + sinceLastSeconds: null, + sinceFirstSeconds: 0 + }; + } + + const sinceLastSeconds = Math.round((now - state.last) / 1000); + const sinceFirstSeconds = Math.round((now - state.first) / 1000); + state.count++; + state.last = now; + + return { + count: state.count, + first: state.first, + sinceLastSeconds, + sinceFirstSeconds + }; +} + +export function detectBot() { + try { + return { + detected: !!( + isWebdriverEnabled() || + window.__nightmare || + window.callPhantom || + window._phantom || + /HeadlessChrome/.test(navigator.userAgent) + ) + }; + } catch (e) { + return { detected: false }; + } +} + +export function getPageVisibility() { + try { + return { + hidden: document.hidden, + state: document.visibilityState, + hasFocus: document.hasFocus() + }; + } catch (e) { + return { hidden: false, state: 'visible', hasFocus: true }; + } +} + const converter = ortbConverter({ context: { netRevenue: true, @@ -108,7 +170,7 @@ const converter = ortbConverter({ }, request(buildRequest, imps, bidderRequest, context) { const reqData = buildRequest(imps, bidderRequest, context); - fillTaboolaReqData(bidderRequest, context.bidRequests[0], reqData) + fillTaboolaReqData(bidderRequest, context.bidRequests[0], reqData, context) return reqData; }, bidResponse(buildBidResponse, bid, context) { @@ -137,7 +199,12 @@ export const spec = { }, buildRequests: (validBidRequests, bidderRequest) => { const [bidRequest] = validBidRequests; - const data = converter.toORTB({bidderRequest: bidderRequest, bidRequests: validBidRequests}); + const auctionId = bidderRequest.auctionId || validBidRequests[0]?.auctionId; + const data = converter.toORTB({ + bidderRequest: bidderRequest, + bidRequests: validBidRequests, + context: { auctionId } + }); const {publisherId} = bidRequest.params; const url = END_POINT_URL + '?publisher=' + publisherId; @@ -287,10 +354,23 @@ function getSiteProperties({publisherId}, refererInfo, ortb2) { } } -function fillTaboolaReqData(bidderRequest, bidRequest, data) { +function fillTaboolaReqData(bidderRequest, bidRequest, data, context) { const {refererInfo, gdprConsent = {}, uspConsent} = bidderRequest; const site = getSiteProperties(bidRequest.params, refererInfo, bidderRequest.ortb2); - deepSetValue(data, 'device', bidderRequest?.ortb2?.device); + + const ortb2Device = bidderRequest?.ortb2?.device || {}; + const connectionType = getConnectionType(); + const device = { + ...ortb2Device, + js: 1, + ...(connectionType && { connectiontype: connectionType }), + ext: { + ...ortb2Device.ext, + bot: detectBot(), + visibility: getPageVisibility() + } + }; + deepSetValue(data, 'device', device); const extractedUserId = userData.getUserId(gdprConsent, uspConsent); if (data.user === undefined || data.user === null) { data.user = { @@ -340,6 +420,10 @@ function fillTaboolaReqData(bidderRequest, bidRequest, data) { data.wlang = ortb2.wlang || bidRequest.params.wlang || []; deepSetValue(data, 'ext.pageType', ortb2?.ext?.data?.pageType || ortb2?.ext?.data?.section || bidRequest.params.pageType); deepSetValue(data, 'ext.prebid.version', '$prebid.version$'); + const auctionId = context?.auctionId; + if (auctionId) { + deepSetValue(data, 'ext.prebid.auctionId', auctionId); + } } function fillTaboolaImpData(bid, imp) { @@ -362,6 +446,18 @@ function fillTaboolaImpData(bid, imp) { imp.bidfloorcur = bidfloorcur; } deepSetValue(imp, 'ext.gpid', bid?.ortb2Imp?.ext?.gpid); + + if (bid.bidId) { + deepSetValue(imp, 'ext.prebid.bidId', bid.bidId); + } + if (bid.adUnitCode) { + deepSetValue(imp, 'ext.prebid.adUnitCode', bid.adUnitCode); + const refreshInfo = getRefreshInfo(bid.adUnitCode); + deepSetValue(imp, 'ext.prebid.refresh', refreshInfo); + } + if (bid.adUnitId) { + deepSetValue(imp, 'ext.prebid.adUnitId', bid.adUnitId); + } } function getBanners(bid, pos) { diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 9c06d717a04..21aad58ac46 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec, internal, END_POINT_URL, userData, EVENT_ENDPOINT} from 'modules/taboolaBidAdapter.js'; +import {spec, internal, END_POINT_URL, userData, EVENT_ENDPOINT, detectBot, getPageVisibility, refreshState, getRefreshInfo} from 'modules/taboolaBidAdapter.js'; import {config} from '../../../src/config.js' import * as utils from '../../../src/utils.js' import {server} from '../../mocks/xhr.js' @@ -285,9 +285,13 @@ describe('Taboola Adapter', function () { 'tagid': commonBidRequest.params.tagId, 'bidfloor': null, 'bidfloorcur': 'USD', - 'ext': {} + 'ext': { + 'prebid': { + 'bidId': defaultBidRequest.bidId + } + } }], - 'device': {'ua': navigator.userAgent}, + 'device': res.data.device, 'id': 'mock-uuid', 'test': 0, 'user': { @@ -308,15 +312,12 @@ describe('Taboola Adapter', function () { 'bcat': [], 'badv': [], 'wlang': [], - 'ext': { - 'prebid': { - 'version': '$prebid.version$' - } - } + 'ext': res.data.ext }; expect(res.url).to.equal(`${END_POINT_URL}?publisher=${commonBidRequest.params.publisherId}`); expect(JSON.stringify(res.data)).to.deep.equal(JSON.stringify(expectedData)); + expect(res.data.ext.prebid.version).to.equal('$prebid.version$'); }); it('should pass optional parameters in request', function () { @@ -475,7 +476,12 @@ describe('Taboola Adapter', function () { expect(res.data.badv).to.deep.equal(bidderRequest.ortb2.badv) expect(res.data.wlang).to.deep.equal(bidderRequest.ortb2.wlang) expect(res.data.user.id).to.deep.equal(bidderRequest.ortb2.user.id) - expect(res.data.device).to.deep.equal(bidderRequest.ortb2.device); + // Device should contain ortb2 device properties plus fraud prevention signals + expect(res.data.device.w).to.equal(bidderRequest.ortb2.device.w); + expect(res.data.device.h).to.equal(bidderRequest.ortb2.device.h); + expect(res.data.device.ua).to.equal(bidderRequest.ortb2.device.ua); + expect(res.data.device.ext.bot).to.exist; + expect(res.data.device.ext.visibility).to.exist; }); it('should pass user entities', function () { @@ -1606,5 +1612,360 @@ describe('Taboola Adapter', function () { expect(internal.getReferrer(bidderRequest.refererInfo)).to.equal(bidderRequest.refererInfo.ref); }); }); + + describe('detectBot', function () { + it('should return detected false for normal browsers', function () { + const result = detectBot(); + expect(result).to.have.property('detected'); + expect(result.detected).to.be.a('boolean'); + }); + + it('should return object with detected property', function () { + const result = detectBot(); + expect(result).to.be.an('object'); + expect(result).to.have.property('detected'); + }); + }); + + describe('getPageVisibility', function () { + it('should return visibility state object', function () { + const result = getPageVisibility(); + expect(result).to.be.an('object'); + expect(result).to.have.property('hidden'); + expect(result).to.have.property('state'); + expect(result).to.have.property('hasFocus'); + }); + + it('should return boolean for hidden property', function () { + const result = getPageVisibility(); + expect(result.hidden).to.be.a('boolean'); + }); + + it('should return string for state property', function () { + const result = getPageVisibility(); + expect(result.state).to.be.a('string'); + }); + + it('should return boolean for hasFocus property', function () { + const result = getPageVisibility(); + expect(result.hasFocus).to.be.a('boolean'); + }); + }); + + describe('fraud signals in buildRequests', function () { + const defaultBidRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'placement name' + }, + bidId: 'test-bid-id', + auctionId: 'test-auction-id', + sizes: [[300, 250]] + }; + + const commonBidderRequest = { + bidderRequestId: 'mock-uuid', + refererInfo: { + page: 'https://example.com/ref', + ref: 'https://ref', + domain: 'example.com', + } + }; + + it('should include bot detection in device.ext', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.device.ext.bot).to.exist; + expect(res.data.device.ext.bot).to.have.property('detected'); + }); + + it('should include visibility in device.ext', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.device.ext.visibility).to.exist; + expect(res.data.device.ext.visibility).to.have.property('hidden'); + expect(res.data.device.ext.visibility).to.have.property('state'); + expect(res.data.device.ext.visibility).to.have.property('hasFocus'); + }); + + it('should preserve existing ortb2 device ext properties', function () { + const bidderRequestWithDeviceExt = { + ...commonBidderRequest, + ortb2: { + device: { + ua: 'test-ua', + ext: { + existingProp: 'existingValue' + } + } + } + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequestWithDeviceExt); + expect(res.data.device.ext.existingProp).to.equal('existingValue'); + expect(res.data.device.ext.bot).to.exist; + expect(res.data.device.ext.visibility).to.exist; + }); + + it('should include device.js = 1', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.device.js).to.equal(1); + }); + + it('should include connectiontype when available', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + // connectiontype is optional - depends on navigator.connection availability + if (res.data.device.connectiontype !== undefined) { + expect(res.data.device.connectiontype).to.be.a('number'); + expect(res.data.device.connectiontype).to.be.oneOf([0, 1, 2, 3, 4, 5, 6, 7]); + } + }); + + it('should not override existing ortb2 device properties', function () { + const bidderRequestWithDevice = { + ...commonBidderRequest, + ortb2: { + device: { + ua: 'custom-ua', + w: 1920, + h: 1080 + } + } + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequestWithDevice); + expect(res.data.device.ua).to.equal('custom-ua'); + expect(res.data.device.w).to.equal(1920); + expect(res.data.device.h).to.equal(1080); + expect(res.data.device.js).to.equal(1); + }); + }); + + describe('Prebid IDs in buildRequests', function () { + const defaultBidRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'placement name' + }, + bidId: 'test-bid-id-123', + auctionId: 'test-auction-id-456', + adUnitCode: 'test-ad-unit-code', + sizes: [[300, 250]] + }; + + const commonBidderRequest = { + bidderRequestId: 'mock-uuid', + auctionId: 'auction-id-789', + refererInfo: { + page: 'https://example.com/ref', + ref: 'https://ref', + domain: 'example.com', + } + }; + + it('should include auctionId in ext.prebid', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.ext.prebid.auctionId).to.equal('auction-id-789'); + }); + + it('should include bidId in imp.ext.prebid', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.bidId).to.equal('test-bid-id-123'); + }); + + it('should include adUnitCode in imp.ext.prebid', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.adUnitCode).to.equal('test-ad-unit-code'); + }); + + it('should include adUnitId in imp.ext.prebid when available', function () { + const bidRequestWithAdUnitId = { + ...defaultBidRequest, + adUnitId: 'test-ad-unit-id' + }; + const res = spec.buildRequests([bidRequestWithAdUnitId], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.adUnitId).to.equal('test-ad-unit-id'); + }); + + it('should not include adUnitId when not available', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.adUnitId).to.be.undefined; + }); + + it('should include all Prebid IDs for multiple impressions', function () { + const bidRequest1 = { + ...defaultBidRequest, + bidId: 'bid-id-1', + adUnitCode: 'ad-unit-1' + }; + const bidRequest2 = { + ...defaultBidRequest, + bidId: 'bid-id-2', + adUnitCode: 'ad-unit-2' + }; + const res = spec.buildRequests([bidRequest1, bidRequest2], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.bidId).to.equal('bid-id-1'); + expect(res.data.imp[0].ext.prebid.adUnitCode).to.equal('ad-unit-1'); + expect(res.data.imp[1].ext.prebid.bidId).to.equal('bid-id-2'); + expect(res.data.imp[1].ext.prebid.adUnitCode).to.equal('ad-unit-2'); + }); + }); + + describe('Refresh tracking in buildRequests', function () { + const defaultBidRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'placement name' + }, + bidId: 'test-bid-id', + adUnitCode: 'refresh-test-ad-unit', + sizes: [[300, 250]] + }; + + const commonBidderRequest = { + bidderRequestId: 'mock-uuid', + auctionId: 'test-auction-id', + refererInfo: { + page: 'https://example.com/ref', + ref: 'https://ref', + domain: 'example.com', + } + }; + + beforeEach(function () { + Object.keys(refreshState).forEach(key => delete refreshState[key]); + }); + + describe('getRefreshInfo function', function () { + it('should return count=1 and sinceLastSeconds=null for first request', function () { + const adUnitCode = 'first-request-test'; + const result = getRefreshInfo(adUnitCode); + + expect(result.count).to.equal(1); + expect(result.sinceLastSeconds).to.be.null; + expect(result.sinceFirstSeconds).to.equal(0); + expect(result.first).to.be.a('number'); + }); + + it('should increment count on subsequent requests', function () { + const adUnitCode = 'increment-test'; + + const first = getRefreshInfo(adUnitCode); + expect(first.count).to.equal(1); + + const second = getRefreshInfo(adUnitCode); + expect(second.count).to.equal(2); + + const third = getRefreshInfo(adUnitCode); + expect(third.count).to.equal(3); + }); + + it('should track different ad units independently', function () { + const adUnit1 = 'independent-test-1'; + const adUnit2 = 'independent-test-2'; + + const first1 = getRefreshInfo(adUnit1); + const first2 = getRefreshInfo(adUnit2); + + expect(first1.count).to.equal(1); + expect(first2.count).to.equal(1); + + const second1 = getRefreshInfo(adUnit1); + expect(second1.count).to.equal(2); + expect(refreshState[adUnit2].count).to.equal(1); + }); + + it('should calculate time differences for subsequent requests', function () { + const adUnitCode = 'time-diff-test'; + const now = Date.now(); + + // Manually set the state to simulate time passing + refreshState[adUnitCode] = { + count: 1, + first: now - 10000, // 10 seconds ago + last: now - 5000 // 5 seconds ago + }; + + const result = getRefreshInfo(adUnitCode); + + expect(result.count).to.equal(2); + expect(result.sinceLastSeconds).to.be.at.least(4); // Allow for timing variance + expect(result.sinceLastSeconds).to.be.at.most(6); + expect(result.sinceFirstSeconds).to.be.at.least(9); + expect(result.sinceFirstSeconds).to.be.at.most(11); + }); + + it('should preserve the first timestamp across refreshes', function () { + const adUnitCode = 'preserve-first-test'; + + const first = getRefreshInfo(adUnitCode); + const firstTimestamp = first.first; + + const second = getRefreshInfo(adUnitCode); + expect(second.first).to.equal(firstTimestamp); + + const third = getRefreshInfo(adUnitCode); + expect(third.first).to.equal(firstTimestamp); + }); + }); + + describe('refresh info in buildRequests', function () { + it('should include refresh info in imp.ext.prebid.refresh', function () { + const bidRequest = { + ...defaultBidRequest, + adUnitCode: 'build-request-refresh-test' + }; + const res = spec.buildRequests([bidRequest], commonBidderRequest); + + expect(res.data.imp[0].ext.prebid.refresh).to.exist; + expect(res.data.imp[0].ext.prebid.refresh.count).to.equal(1); + expect(res.data.imp[0].ext.prebid.refresh.sinceLastSeconds).to.be.null; + expect(res.data.imp[0].ext.prebid.refresh.sinceFirstSeconds).to.equal(0); + expect(res.data.imp[0].ext.prebid.refresh.first).to.be.a('number'); + }); + + it('should increment refresh count on subsequent builds for same ad unit', function () { + const bidRequest = { + ...defaultBidRequest, + adUnitCode: 'build-request-increment-test' + }; + + const res1 = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res1.data.imp[0].ext.prebid.refresh.count).to.equal(1); + + const res2 = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res2.data.imp[0].ext.prebid.refresh.count).to.equal(2); + + const res3 = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res3.data.imp[0].ext.prebid.refresh.count).to.equal(3); + }); + + it('should track refresh independently for multiple ad units in same request', function () { + const bidRequest1 = { + ...defaultBidRequest, + bidId: 'bid-1', + adUnitCode: 'multi-ad-unit-test-1' + }; + const bidRequest2 = { + ...defaultBidRequest, + bidId: 'bid-2', + adUnitCode: 'multi-ad-unit-test-2' + }; + + const res = spec.buildRequests([bidRequest1, bidRequest2], commonBidderRequest); + + expect(res.data.imp[0].ext.prebid.refresh.count).to.equal(1); + expect(res.data.imp[1].ext.prebid.refresh.count).to.equal(1); + + // Second request - only refresh first ad unit + const res2 = spec.buildRequests([bidRequest1], commonBidderRequest); + expect(res2.data.imp[0].ext.prebid.refresh.count).to.equal(2); + + // Third request - both ad units + const res3 = spec.buildRequests([bidRequest1, bidRequest2], commonBidderRequest); + expect(res3.data.imp[0].ext.prebid.refresh.count).to.equal(3); + expect(res3.data.imp[1].ext.prebid.refresh.count).to.equal(2); + }); + }); + }); }) }) From ef52a90c96b6f5175d7f26231354a643e7312f1f Mon Sep 17 00:00:00 2001 From: Tal Avital Date: Thu, 25 Dec 2025 13:43:12 +0200 Subject: [PATCH 05/15] add extra ad signals --- modules/taboolaBidAdapter.js | 60 ++++++++++-- test/spec/modules/taboolaBidAdapter_spec.js | 102 ++++++++++++++++++++ 2 files changed, 156 insertions(+), 6 deletions(-) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 79e3291ac66..88c9eb97e45 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -3,12 +3,15 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; -import {deepSetValue, getWindowSelf, replaceAuctionPrice, isArray, safeJSONParse, isPlainObject} from '../src/utils.js'; +import {deepSetValue, getWindowSelf, replaceAuctionPrice, isArray, safeJSONParse, isPlainObject, getWinDimensions} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; import {isWebdriverEnabled} from '../libraries/webdriver/webdriver.js'; import {getConnectionType} from '../libraries/connectionInfo/connectionUtils.js'; +import {getViewportCoordinates} from '../libraries/viewport/viewport.js'; +import {percentInView} from '../libraries/percentInView/percentInView.js'; +import {getBoundingClientRect} from '../libraries/boundingClientRect/boundingClientRect.js'; const BIDDER_CODE = 'taboola'; const GVLID = 42; @@ -157,6 +160,46 @@ export function getPageVisibility() { } } +export function getDeviceExtSignals(existingExt = {}) { + const viewport = getViewportCoordinates(); + return { + ...existingExt, + bot: detectBot(), + visibility: getPageVisibility(), + scroll: { + top: Math.round(viewport.top), + left: Math.round(viewport.left) + } + }; +} + +export function getElementSignals(adUnitCode) { + try { + const element = document.getElementById(adUnitCode); + if (!element) return null; + + const rect = getBoundingClientRect(element); + const winDimensions = getWinDimensions(); + const rawViewability = percentInView(element); + + const signals = { + placement: { + top: Math.round(rect.top), + left: Math.round(rect.left) + }, + fold: rect.top < winDimensions.innerHeight ? 'above' : 'below' + }; + + if (rawViewability !== null && !isNaN(rawViewability)) { + signals.viewability = Math.round(rawViewability); + } + + return signals; + } catch (e) { + return null; + } +} + const converter = ortbConverter({ context: { netRevenue: true, @@ -364,11 +407,7 @@ function fillTaboolaReqData(bidderRequest, bidRequest, data, context) { ...ortb2Device, js: 1, ...(connectionType && { connectiontype: connectionType }), - ext: { - ...ortb2Device.ext, - bot: detectBot(), - visibility: getPageVisibility() - } + ext: getDeviceExtSignals(ortb2Device.ext) }; deepSetValue(data, 'device', device); const extractedUserId = userData.getUserId(gdprConsent, uspConsent); @@ -458,6 +497,15 @@ function fillTaboolaImpData(bid, imp) { if (bid.adUnitId) { deepSetValue(imp, 'ext.prebid.adUnitId', bid.adUnitId); } + + const elementSignals = getElementSignals(bid.adUnitCode); + if (elementSignals) { + if (elementSignals.viewability !== undefined) { + deepSetValue(imp, 'ext.viewability', elementSignals.viewability); + } + deepSetValue(imp, 'ext.placement', elementSignals.placement); + deepSetValue(imp, 'ext.fold', elementSignals.fold); + } } function getBanners(bid, pos) { diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 21aad58ac46..8aaff32c380 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -1687,6 +1687,108 @@ describe('Taboola Adapter', function () { expect(res.data.device.ext.visibility).to.have.property('hasFocus'); }); + it('should include scroll position in device.ext', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.device.ext.scroll).to.exist; + expect(res.data.device.ext.scroll).to.have.property('top'); + expect(res.data.device.ext.scroll).to.have.property('left'); + expect(res.data.device.ext.scroll.top).to.be.a('number'); + expect(res.data.device.ext.scroll.left).to.be.a('number'); + }); + + it('should include viewability in imp.ext when element exists', function () { + const adUnitCode = 'test-viewability-div'; + const testDiv = document.createElement('div'); + testDiv.id = adUnitCode; + testDiv.style.width = '300px'; + testDiv.style.height = '250px'; + document.body.appendChild(testDiv); + + const bidRequest = { + ...defaultBidRequest, + adUnitCode: adUnitCode + }; + + try { + const res = spec.buildRequests([bidRequest], commonBidderRequest); + // Viewability should be a number between 0-100 when element exists + expect(res.data.imp[0].ext.viewability).to.be.a('number'); + expect(res.data.imp[0].ext.viewability).to.be.at.least(0); + expect(res.data.imp[0].ext.viewability).to.be.at.most(100); + } finally { + document.body.removeChild(testDiv); + } + }); + + it('should not include viewability when element does not exist', function () { + const bidRequest = { + ...defaultBidRequest, + adUnitCode: 'non-existent-element-id' + }; + const res = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.viewability).to.be.undefined; + }); + + it('should include placement position in imp.ext when element exists', function () { + const adUnitCode = 'test-placement-div'; + const testDiv = document.createElement('div'); + testDiv.id = adUnitCode; + testDiv.style.width = '300px'; + testDiv.style.height = '250px'; + testDiv.style.position = 'absolute'; + testDiv.style.top = '100px'; + testDiv.style.left = '50px'; + document.body.appendChild(testDiv); + + const bidRequest = { + ...defaultBidRequest, + adUnitCode: adUnitCode + }; + + try { + const res = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.placement).to.exist; + expect(res.data.imp[0].ext.placement).to.have.property('top'); + expect(res.data.imp[0].ext.placement).to.have.property('left'); + expect(res.data.imp[0].ext.placement.top).to.be.a('number'); + expect(res.data.imp[0].ext.placement.left).to.be.a('number'); + } finally { + document.body.removeChild(testDiv); + } + }); + + it('should include fold detection in imp.ext when element exists', function () { + const adUnitCode = 'test-fold-div'; + const testDiv = document.createElement('div'); + testDiv.id = adUnitCode; + testDiv.style.width = '300px'; + testDiv.style.height = '250px'; + document.body.appendChild(testDiv); + + const bidRequest = { + ...defaultBidRequest, + adUnitCode: adUnitCode + }; + + try { + const res = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.fold).to.exist; + expect(res.data.imp[0].ext.fold).to.be.oneOf(['above', 'below']); + } finally { + document.body.removeChild(testDiv); + } + }); + + it('should not include placement or fold when element does not exist', function () { + const bidRequest = { + ...defaultBidRequest, + adUnitCode: 'non-existent-placement-element' + }; + const res = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.placement).to.be.undefined; + expect(res.data.imp[0].ext.fold).to.be.undefined; + }); + it('should preserve existing ortb2 device ext properties', function () { const bidderRequestWithDeviceExt = { ...commonBidderRequest, From 090f4421e23c3644bb3773db5e58a1f94a29f207 Mon Sep 17 00:00:00 2001 From: Tal Avital Date: Sun, 28 Dec 2025 14:48:43 +0200 Subject: [PATCH 06/15] fix missing semicolon --- modules/taboolaBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 88c9eb97e45..ecc450ca2a5 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -213,7 +213,7 @@ const converter = ortbConverter({ }, request(buildRequest, imps, bidderRequest, context) { const reqData = buildRequest(imps, bidderRequest, context); - fillTaboolaReqData(bidderRequest, context.bidRequests[0], reqData, context) + fillTaboolaReqData(bidderRequest, context.bidRequests[0], reqData, context); return reqData; }, bidResponse(buildBidResponse, bid, context) { From 124fda15140a9a2ed7a44ed99b5146bc815f891b Mon Sep 17 00:00:00 2001 From: Tal Avital Date: Tue, 13 Jan 2026 14:53:01 +0200 Subject: [PATCH 07/15] use Prebid's built-in counters --- modules/taboolaBidAdapter.js | 38 +---- test/spec/modules/taboolaBidAdapter_spec.js | 170 +++++--------------- 2 files changed, 44 insertions(+), 164 deletions(-) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index ecc450ca2a5..43c91b17956 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -26,7 +26,6 @@ const TGID_COOKIE_KEY = 't_gid'; const TGID_PT_COOKIE_KEY = 't_pt_gid'; const TBLA_ID_COOKIE_KEY = 'tbla_id'; export const EVENT_ENDPOINT = 'https://beacon.bidder.taboola.com'; -export const refreshState = {}; /** * extract User Id by that order: @@ -101,37 +100,6 @@ export const internal = { } } -export function getRefreshInfo(adUnitCode) { - const now = Date.now(); - const state = refreshState[adUnitCode]; - - if (!state) { - refreshState[adUnitCode] = { - count: 1, - first: now, - last: now - }; - return { - count: 1, - first: now, - sinceLastSeconds: null, - sinceFirstSeconds: 0 - }; - } - - const sinceLastSeconds = Math.round((now - state.last) / 1000); - const sinceFirstSeconds = Math.round((now - state.first) / 1000); - state.count++; - state.last = now; - - return { - count: state.count, - first: state.first, - sinceLastSeconds, - sinceFirstSeconds - }; -} - export function detectBot() { try { return { @@ -491,13 +459,15 @@ function fillTaboolaImpData(bid, imp) { } if (bid.adUnitCode) { deepSetValue(imp, 'ext.prebid.adUnitCode', bid.adUnitCode); - const refreshInfo = getRefreshInfo(bid.adUnitCode); - deepSetValue(imp, 'ext.prebid.refresh', refreshInfo); } if (bid.adUnitId) { deepSetValue(imp, 'ext.prebid.adUnitId', bid.adUnitId); } + deepSetValue(imp, 'ext.prebid.bidRequestsCount', bid.bidRequestsCount); + deepSetValue(imp, 'ext.prebid.bidderRequestsCount', bid.bidderRequestsCount); + deepSetValue(imp, 'ext.prebid.bidderWinsCount', bid.bidderWinsCount); + const elementSignals = getElementSignals(bid.adUnitCode); if (elementSignals) { if (elementSignals.viewability !== undefined) { diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 8aaff32c380..8c23be4bcd0 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec, internal, END_POINT_URL, userData, EVENT_ENDPOINT, detectBot, getPageVisibility, refreshState, getRefreshInfo} from 'modules/taboolaBidAdapter.js'; +import {spec, internal, END_POINT_URL, userData, EVENT_ENDPOINT, detectBot, getPageVisibility} from 'modules/taboolaBidAdapter.js'; import {config} from '../../../src/config.js' import * as utils from '../../../src/utils.js' import {server} from '../../mocks/xhr.js' @@ -1911,7 +1911,7 @@ describe('Taboola Adapter', function () { }); }); - describe('Refresh tracking in buildRequests', function () { + describe('Prebid counters in buildRequests', function () { const defaultBidRequest = { bidder: 'taboola', params: { @@ -1919,7 +1919,10 @@ describe('Taboola Adapter', function () { tagId: 'placement name' }, bidId: 'test-bid-id', - adUnitCode: 'refresh-test-ad-unit', + adUnitCode: 'test-ad-unit', + bidRequestsCount: 3, + bidderRequestsCount: 2, + bidderWinsCount: 1, sizes: [[300, 250]] }; @@ -1933,140 +1936,47 @@ describe('Taboola Adapter', function () { } }; - beforeEach(function () { - Object.keys(refreshState).forEach(key => delete refreshState[key]); + it('should include bidRequestsCount in imp.ext.prebid', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.bidRequestsCount).to.equal(3); }); - describe('getRefreshInfo function', function () { - it('should return count=1 and sinceLastSeconds=null for first request', function () { - const adUnitCode = 'first-request-test'; - const result = getRefreshInfo(adUnitCode); - - expect(result.count).to.equal(1); - expect(result.sinceLastSeconds).to.be.null; - expect(result.sinceFirstSeconds).to.equal(0); - expect(result.first).to.be.a('number'); - }); - - it('should increment count on subsequent requests', function () { - const adUnitCode = 'increment-test'; - - const first = getRefreshInfo(adUnitCode); - expect(first.count).to.equal(1); - - const second = getRefreshInfo(adUnitCode); - expect(second.count).to.equal(2); - - const third = getRefreshInfo(adUnitCode); - expect(third.count).to.equal(3); - }); - - it('should track different ad units independently', function () { - const adUnit1 = 'independent-test-1'; - const adUnit2 = 'independent-test-2'; - - const first1 = getRefreshInfo(adUnit1); - const first2 = getRefreshInfo(adUnit2); - - expect(first1.count).to.equal(1); - expect(first2.count).to.equal(1); - - const second1 = getRefreshInfo(adUnit1); - expect(second1.count).to.equal(2); - expect(refreshState[adUnit2].count).to.equal(1); - }); - - it('should calculate time differences for subsequent requests', function () { - const adUnitCode = 'time-diff-test'; - const now = Date.now(); - - // Manually set the state to simulate time passing - refreshState[adUnitCode] = { - count: 1, - first: now - 10000, // 10 seconds ago - last: now - 5000 // 5 seconds ago - }; - - const result = getRefreshInfo(adUnitCode); - - expect(result.count).to.equal(2); - expect(result.sinceLastSeconds).to.be.at.least(4); // Allow for timing variance - expect(result.sinceLastSeconds).to.be.at.most(6); - expect(result.sinceFirstSeconds).to.be.at.least(9); - expect(result.sinceFirstSeconds).to.be.at.most(11); - }); - - it('should preserve the first timestamp across refreshes', function () { - const adUnitCode = 'preserve-first-test'; - - const first = getRefreshInfo(adUnitCode); - const firstTimestamp = first.first; - - const second = getRefreshInfo(adUnitCode); - expect(second.first).to.equal(firstTimestamp); - - const third = getRefreshInfo(adUnitCode); - expect(third.first).to.equal(firstTimestamp); - }); + it('should include bidderRequestsCount in imp.ext.prebid', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.bidderRequestsCount).to.equal(2); }); - describe('refresh info in buildRequests', function () { - it('should include refresh info in imp.ext.prebid.refresh', function () { - const bidRequest = { - ...defaultBidRequest, - adUnitCode: 'build-request-refresh-test' - }; - const res = spec.buildRequests([bidRequest], commonBidderRequest); - - expect(res.data.imp[0].ext.prebid.refresh).to.exist; - expect(res.data.imp[0].ext.prebid.refresh.count).to.equal(1); - expect(res.data.imp[0].ext.prebid.refresh.sinceLastSeconds).to.be.null; - expect(res.data.imp[0].ext.prebid.refresh.sinceFirstSeconds).to.equal(0); - expect(res.data.imp[0].ext.prebid.refresh.first).to.be.a('number'); - }); - - it('should increment refresh count on subsequent builds for same ad unit', function () { - const bidRequest = { - ...defaultBidRequest, - adUnitCode: 'build-request-increment-test' - }; - - const res1 = spec.buildRequests([bidRequest], commonBidderRequest); - expect(res1.data.imp[0].ext.prebid.refresh.count).to.equal(1); + it('should include bidderWinsCount in imp.ext.prebid', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.bidderWinsCount).to.equal(1); + }); - const res2 = spec.buildRequests([bidRequest], commonBidderRequest); - expect(res2.data.imp[0].ext.prebid.refresh.count).to.equal(2); + it('should include all Prebid counters for multiple impressions', function () { + const bidRequest1 = { + ...defaultBidRequest, + bidId: 'bid-id-1', + adUnitCode: 'ad-unit-1', + bidRequestsCount: 5, + bidderRequestsCount: 4, + bidderWinsCount: 2 + }; + const bidRequest2 = { + ...defaultBidRequest, + bidId: 'bid-id-2', + adUnitCode: 'ad-unit-2', + bidRequestsCount: 2, + bidderRequestsCount: 1, + bidderWinsCount: 0 + }; + const res = spec.buildRequests([bidRequest1, bidRequest2], commonBidderRequest); - const res3 = spec.buildRequests([bidRequest], commonBidderRequest); - expect(res3.data.imp[0].ext.prebid.refresh.count).to.equal(3); - }); + expect(res.data.imp[0].ext.prebid.bidRequestsCount).to.equal(5); + expect(res.data.imp[0].ext.prebid.bidderRequestsCount).to.equal(4); + expect(res.data.imp[0].ext.prebid.bidderWinsCount).to.equal(2); - it('should track refresh independently for multiple ad units in same request', function () { - const bidRequest1 = { - ...defaultBidRequest, - bidId: 'bid-1', - adUnitCode: 'multi-ad-unit-test-1' - }; - const bidRequest2 = { - ...defaultBidRequest, - bidId: 'bid-2', - adUnitCode: 'multi-ad-unit-test-2' - }; - - const res = spec.buildRequests([bidRequest1, bidRequest2], commonBidderRequest); - - expect(res.data.imp[0].ext.prebid.refresh.count).to.equal(1); - expect(res.data.imp[1].ext.prebid.refresh.count).to.equal(1); - - // Second request - only refresh first ad unit - const res2 = spec.buildRequests([bidRequest1], commonBidderRequest); - expect(res2.data.imp[0].ext.prebid.refresh.count).to.equal(2); - - // Third request - both ad units - const res3 = spec.buildRequests([bidRequest1, bidRequest2], commonBidderRequest); - expect(res3.data.imp[0].ext.prebid.refresh.count).to.equal(3); - expect(res3.data.imp[1].ext.prebid.refresh.count).to.equal(2); - }); + expect(res.data.imp[1].ext.prebid.bidRequestsCount).to.equal(2); + expect(res.data.imp[1].ext.prebid.bidderRequestsCount).to.equal(1); + expect(res.data.imp[1].ext.prebid.bidderWinsCount).to.equal(0); }); }); }) From ba8d7809bc2a5b48add0593cc92377998e105844 Mon Sep 17 00:00:00 2001 From: Tal Avital Date: Thu, 15 Jan 2026 11:27:17 +0200 Subject: [PATCH 08/15] updated detectBot logic --- modules/taboolaBidAdapter.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 43c91b17956..421ddbd256b 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -7,7 +7,6 @@ import {deepSetValue, getWindowSelf, replaceAuctionPrice, isArray, safeJSONParse import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; -import {isWebdriverEnabled} from '../libraries/webdriver/webdriver.js'; import {getConnectionType} from '../libraries/connectionInfo/connectionUtils.js'; import {getViewportCoordinates} from '../libraries/viewport/viewport.js'; import {percentInView} from '../libraries/percentInView/percentInView.js'; @@ -104,7 +103,6 @@ export function detectBot() { try { return { detected: !!( - isWebdriverEnabled() || window.__nightmare || window.callPhantom || window._phantom || From d4de9d4395b758b068a48ee948f23cb05160796d Mon Sep 17 00:00:00 2001 From: Roni Shefi Date: Sun, 11 Jan 2026 14:09:46 +0200 Subject: [PATCH 09/15] In Taboola adapter, added support for native and adjusted the existing banner support. Added and updated tests. --- modules/taboolaBidAdapter.js | 46 +++-- test/spec/modules/taboolaBidAdapter_spec.js | 197 +++++++++++++++++++- 2 files changed, 224 insertions(+), 19 deletions(-) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 421ddbd256b..e13085d4a26 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -1,7 +1,7 @@ 'use strict'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {deepSetValue, getWindowSelf, replaceAuctionPrice, isArray, safeJSONParse, isPlainObject, getWinDimensions} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -15,7 +15,8 @@ import {getBoundingClientRect} from '../libraries/boundingClientRect/boundingCli const BIDDER_CODE = 'taboola'; const GVLID = 42; const CURRENCY = 'USD'; -export const END_POINT_URL = 'https://display.bidder.taboola.com/OpenRTB/TaboolaHB/auction'; +export const BANNER_ENDPOINT_URL = 'https://display.bidder.taboola.com/OpenRTB/TaboolaHB/auction'; +export const NATIVE_ENDPOINT_URL = 'https://native.bidder.taboola.com/OpenRTB/TaboolaHB/auction'; export const USER_SYNC_IMG_URL = 'https://trc.taboola.com/sg/prebidJS/1/cm'; export const USER_SYNC_IFRAME_URL = 'https://cdn.taboola.com/scripts/prebid_iframe_sync.html'; const USER_ID = 'user-id'; @@ -169,7 +170,6 @@ export function getElementSignals(adUnitCode) { const converter = ortbConverter({ context: { netRevenue: true, - mediaType: BANNER, ttl: 300 }, imp(buildImp, bidRequest, context) { @@ -183,12 +183,26 @@ const converter = ortbConverter({ return reqData; }, bidResponse(buildBidResponse, bid, context) { + const hasNative = !!context.bidRequest?.mediaTypes?.native; + const hasBanner = !!context.bidRequest?.mediaTypes?.banner; + context.mediaType = hasNative && !hasBanner ? NATIVE : BANNER; + + // Unwrap native response - server returns {native: {...}} but ortbConverter expects {...} + if (context.mediaType === NATIVE) { + const admObj = safeJSONParse(bid.adm); + if (admObj?.native) { + bid.adm = JSON.stringify(admObj.native); + } + } + const bidResponse = buildBidResponse(bid, context); bidResponse.nurl = bid.nurl; if (bid.burl) { bidResponse.burl = bid.burl; } - bidResponse.ad = replaceAuctionPrice(bid.adm, bid.price); + if (bidResponse.mediaType !== NATIVE) { + bidResponse.ad = replaceAuctionPrice(bid.adm, bid.price); + } if (bid.ext && bid.ext.dchain) { deepSetValue(bidResponse, 'meta.dchain', bid.ext.dchain); } @@ -197,14 +211,19 @@ const converter = ortbConverter({ }); export const spec = { - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, NATIVE], gvlid: GVLID, code: BIDDER_CODE, isBidRequestValid: (bidRequest) => { - return !!(bidRequest.sizes && - bidRequest.params && + const hasPublisherAndTag = !!(bidRequest.params && bidRequest.params.publisherId && bidRequest.params.tagId); + if (!hasPublisherAndTag) { + return false; + } + const hasBanner = !!bidRequest.mediaTypes?.banner; + const hasNative = !!bidRequest.mediaTypes?.native; + return hasBanner || hasNative; }, buildRequests: (validBidRequests, bidderRequest) => { const [bidRequest] = validBidRequests; @@ -215,7 +234,9 @@ export const spec = { context: { auctionId } }); const {publisherId} = bidRequest.params; - const url = END_POINT_URL + '?publisher=' + publisherId; + const isNative = !!bidRequest.mediaTypes?.native; + const baseUrl = isNative ? NATIVE_ENDPOINT_URL : BANNER_ENDPOINT_URL; + const url = baseUrl + '?publisher=' + publisherId; return { url, @@ -433,7 +454,10 @@ function fillTaboolaReqData(bidderRequest, bidRequest, data, context) { function fillTaboolaImpData(bid, imp) { const {tagId, position} = bid.params; - imp.banner = getBanners(bid, position); + const bannerSizes = bid.mediaTypes?.banner?.sizes; + if (bannerSizes) { + imp.banner = getBanners(bannerSizes, position); + } imp.tagid = tagId; if (typeof bid.getFloor === 'function') { @@ -476,9 +500,9 @@ function fillTaboolaImpData(bid, imp) { } } -function getBanners(bid, pos) { +function getBanners(sizes, pos) { return { - ...getSizes(bid.sizes), + ...getSizes(sizes), pos: pos } } diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 8c23be4bcd0..1a31c94d7dd 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec, internal, END_POINT_URL, userData, EVENT_ENDPOINT, detectBot, getPageVisibility} from 'modules/taboolaBidAdapter.js'; +import {spec, internal, BANNER_ENDPOINT_URL, userData, EVENT_ENDPOINT, detectBot, getPageVisibility} from 'modules/taboolaBidAdapter.js'; import {config} from '../../../src/config.js' import * as utils from '../../../src/utils.js' import {server} from '../../mocks/xhr.js' @@ -33,7 +33,11 @@ describe('Taboola Adapter', function () { }) const displayBidRequestParams = { - sizes: [[300, 250], [300, 600]] + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + } } const createBidRequest = () => ({ @@ -270,18 +274,18 @@ describe('Taboola Adapter', function () { const expectedData = { 'imp': [{ 'id': res.data.imp[0].id, - 'secure': 1, 'banner': { format: [{ - w: displayBidRequestParams.sizes[0][0], - h: displayBidRequestParams.sizes[0][1] + w: displayBidRequestParams.mediaTypes.banner.sizes[0][0], + h: displayBidRequestParams.mediaTypes.banner.sizes[0][1] }, { - w: displayBidRequestParams.sizes[1][0], - h: displayBidRequestParams.sizes[1][1] + w: displayBidRequestParams.mediaTypes.banner.sizes[1][0], + h: displayBidRequestParams.mediaTypes.banner.sizes[1][1] } ] }, + 'secure': 1, 'tagid': commonBidRequest.params.tagId, 'bidfloor': null, 'bidfloorcur': 'USD', @@ -315,7 +319,7 @@ describe('Taboola Adapter', function () { 'ext': res.data.ext }; - expect(res.url).to.equal(`${END_POINT_URL}?publisher=${commonBidRequest.params.publisherId}`); + expect(res.url).to.equal(`${BANNER_ENDPOINT_URL}?publisher=${commonBidRequest.params.publisherId}`); expect(JSON.stringify(res.data)).to.deep.equal(JSON.stringify(expectedData)); expect(res.data.ext.prebid.version).to.equal('$prebid.version$'); }); @@ -1980,4 +1984,181 @@ describe('Taboola Adapter', function () { }); }); }) + + describe('native', function () { + const commonBidderRequest = { + bidderRequestId: 'mock-uuid', + refererInfo: { + page: 'https://example.com/ref', + ref: 'https://ref', + domain: 'example.com', + }, + ortb2: { + device: { + ua: navigator.userAgent, + }, + } + }; + + const nativeBidRequestParams = { + mediaTypes: { + native: { + title: {required: true, len: 150}, + image: {required: true, sizes: [300, 250]}, + sponsoredBy: {required: true} + } + } + }; + + describe('isBidRequestValid', function () { + it('should return true for valid native bid without sizes', function () { + const bid = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'native-placement' + }, + ...nativeBidRequestParams + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false for native bid without publisherId', function () { + const bid = { + bidder: 'taboola', + params: { + tagId: 'native-placement' + }, + ...nativeBidRequestParams + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false for native bid without tagId', function () { + const bid = { + bidder: 'taboola', + params: { + publisherId: 'publisherId' + }, + ...nativeBidRequestParams + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('should build native request without banner imp', function () { + const nativeBidRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'native-placement' + }, + ...nativeBidRequestParams, + nativeOrtbRequest: { + ver: '1.2', + assets: [ + {id: 1, required: 1, title: {len: 150}}, + {id: 2, required: 1, img: {type: 3, w: 300, h: 250}}, + {id: 3, required: 1, data: {type: 1}} + ] + }, + bidId: utils.generateUUID(), + auctionId: utils.generateUUID(), + }; + + const res = spec.buildRequests([nativeBidRequest], commonBidderRequest); + + expect(res.data.imp[0]).to.not.have.property('banner'); + expect(res.data.imp[0]).to.have.property('native'); // TODO native isn't added! + expect(res.data.imp[0].tagid).to.equal('native-placement'); + }); + + it('should build banner request without native imp', function () { + const bannerBidRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'banner-placement' + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bidId: utils.generateUUID(), + auctionId: utils.generateUUID(), + }; + + const res = spec.buildRequests([bannerBidRequest], commonBidderRequest); + + expect(res.data.imp[0]).to.have.property('banner'); + expect(res.data.imp[0]).to.not.have.property('native'); + expect(res.data.imp[0]).to.not.have.property('native'); + }); + }); + + describe('interpretResponse', function () { + it('should interpret native response correctly', function () { + const nativeBidRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'native-placement' + }, + ...nativeBidRequestParams, + nativeOrtbRequest: { + ver: '1.2', + assets: [ + {id: 1, required: 1, title: {len: 150}}, + {id: 2, required: 1, img: {type: 3, w: 300, h: 250}} + ] + }, + bidId: utils.generateUUID(), + auctionId: utils.generateUUID(), + }; + + const request = spec.buildRequests([nativeBidRequest], commonBidderRequest); + + const nativeAdm = { + ver: '1.2', + assets: [ + {id: 1, title: {text: 'Native Ad Title'}}, + {id: 2, img: {url: 'https://example.com/image.jpg', w: 300, h: 250}} + ], + link: { + url: 'https://example.com/click' + } + }; + + const serverResponse = { + body: { + id: 'response-id', + seatbid: [{ + bid: [{ + id: 'bid-id', + impid: request.data.imp[0].id, + price: 1.5, + adm: JSON.stringify(nativeAdm), + adomain: ['example.com'], + crid: 'creative-id', + exp: 300, + nurl: 'https://example.com/win' + }], + seat: 'taboola' + }], + cur: 'USD' + } + }; + + const res = spec.interpretResponse(serverResponse, request); + + expect(res).to.be.an('array').with.lengthOf(1); + expect(res[0].mediaType).to.equal('native'); + expect(res[0].native).to.exist; + expect(res[0].native.ortb).to.deep.equal(nativeAdm); + expect(res[0]).to.not.have.property('ad'); + }); + }); + }); }) From d7fae2386bc7f9f1c3fae3a603299e671074a854 Mon Sep 17 00:00:00 2001 From: Roni Shefi Date: Sun, 11 Jan 2026 14:05:39 +0200 Subject: [PATCH 10/15] In Taboola adapter, added support for native and adjusted the existing banner support. Added and updated tests. --- .../gpt/taboola_native_test.html | 164 ++++++++++++++++++ modules/taboolaBidAdapter.js | 1 - 2 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 integrationExamples/gpt/taboola_native_test.html diff --git a/integrationExamples/gpt/taboola_native_test.html b/integrationExamples/gpt/taboola_native_test.html new file mode 100644 index 00000000000..e8444d82ced --- /dev/null +++ b/integrationExamples/gpt/taboola_native_test.html @@ -0,0 +1,164 @@ + + + Taboola Native Prebid.js Example + + + + + + +

Taboola Native Prebid.js Example

+
Native Ad Container
+
+

Loading native ad...

+
+ + diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index e13085d4a26..d6da9f3fc50 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -187,7 +187,6 @@ const converter = ortbConverter({ const hasBanner = !!context.bidRequest?.mediaTypes?.banner; context.mediaType = hasNative && !hasBanner ? NATIVE : BANNER; - // Unwrap native response - server returns {native: {...}} but ortbConverter expects {...} if (context.mediaType === NATIVE) { const admObj = safeJSONParse(bid.adm); if (admObj?.native) { From 53fe2c5767939a8b7f7ba09a5335d581536aa470 Mon Sep 17 00:00:00 2001 From: Roni Shefi Date: Sun, 11 Jan 2026 14:37:25 +0200 Subject: [PATCH 11/15] removed test page pushed accidentally --- .../gpt/taboola_native_test.html | 164 ------------------ 1 file changed, 164 deletions(-) delete mode 100644 integrationExamples/gpt/taboola_native_test.html diff --git a/integrationExamples/gpt/taboola_native_test.html b/integrationExamples/gpt/taboola_native_test.html deleted file mode 100644 index e8444d82ced..00000000000 --- a/integrationExamples/gpt/taboola_native_test.html +++ /dev/null @@ -1,164 +0,0 @@ - - - Taboola Native Prebid.js Example - - - - - - -

Taboola Native Prebid.js Example

-
Native Ad Container
-
-

Loading native ad...

-
- - From a9754e9d53999f7241dd59b0bf4fa1c36f13bd50 Mon Sep 17 00:00:00 2001 From: Roni Shefi Date: Sun, 11 Jan 2026 16:27:30 +0200 Subject: [PATCH 12/15] Wrapped native tests with NATIVE feature check --- test/spec/modules/taboolaBidAdapter_spec.js | 162 ++++++++++---------- 1 file changed, 83 insertions(+), 79 deletions(-) diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 1a31c94d7dd..e488ffacef2 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -2047,32 +2047,34 @@ describe('Taboola Adapter', function () { }); describe('buildRequests', function () { - it('should build native request without banner imp', function () { - const nativeBidRequest = { - bidder: 'taboola', - params: { - publisherId: 'publisherId', - tagId: 'native-placement' - }, - ...nativeBidRequestParams, - nativeOrtbRequest: { - ver: '1.2', - assets: [ - {id: 1, required: 1, title: {len: 150}}, - {id: 2, required: 1, img: {type: 3, w: 300, h: 250}}, - {id: 3, required: 1, data: {type: 1}} - ] - }, - bidId: utils.generateUUID(), - auctionId: utils.generateUUID(), - }; + if (FEATURES.NATIVE) { + it('should build native request without banner imp', function () { + const nativeBidRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'native-placement' + }, + ...nativeBidRequestParams, + nativeOrtbRequest: { + ver: '1.2', + assets: [ + {id: 1, required: 1, title: {len: 150}}, + {id: 2, required: 1, img: {type: 3, w: 300, h: 250}}, + {id: 3, required: 1, data: {type: 1}} + ] + }, + bidId: utils.generateUUID(), + auctionId: utils.generateUUID(), + }; - const res = spec.buildRequests([nativeBidRequest], commonBidderRequest); + const res = spec.buildRequests([nativeBidRequest], commonBidderRequest); - expect(res.data.imp[0]).to.not.have.property('banner'); - expect(res.data.imp[0]).to.have.property('native'); // TODO native isn't added! - expect(res.data.imp[0].tagid).to.equal('native-placement'); - }); + expect(res.data.imp[0]).to.not.have.property('banner'); + expect(res.data.imp[0]).to.have.property('native'); + expect(res.data.imp[0].tagid).to.equal('native-placement'); + }); + } it('should build banner request without native imp', function () { const bannerBidRequest = { @@ -2099,66 +2101,68 @@ describe('Taboola Adapter', function () { }); describe('interpretResponse', function () { - it('should interpret native response correctly', function () { - const nativeBidRequest = { - bidder: 'taboola', - params: { - publisherId: 'publisherId', - tagId: 'native-placement' - }, - ...nativeBidRequestParams, - nativeOrtbRequest: { - ver: '1.2', - assets: [ - {id: 1, required: 1, title: {len: 150}}, - {id: 2, required: 1, img: {type: 3, w: 300, h: 250}} - ] - }, - bidId: utils.generateUUID(), - auctionId: utils.generateUUID(), - }; + if (FEATURES.NATIVE) { + it('should interpret native response correctly', function () { + const nativeBidRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'native-placement' + }, + ...nativeBidRequestParams, + nativeOrtbRequest: { + ver: '1.2', + assets: [ + {id: 1, required: 1, title: {len: 150}}, + {id: 2, required: 1, img: {type: 3, w: 300, h: 250}} + ] + }, + bidId: utils.generateUUID(), + auctionId: utils.generateUUID(), + }; - const request = spec.buildRequests([nativeBidRequest], commonBidderRequest); + const request = spec.buildRequests([nativeBidRequest], commonBidderRequest); - const nativeAdm = { - ver: '1.2', - assets: [ - {id: 1, title: {text: 'Native Ad Title'}}, - {id: 2, img: {url: 'https://example.com/image.jpg', w: 300, h: 250}} - ], - link: { - url: 'https://example.com/click' - } - }; - - const serverResponse = { - body: { - id: 'response-id', - seatbid: [{ - bid: [{ - id: 'bid-id', - impid: request.data.imp[0].id, - price: 1.5, - adm: JSON.stringify(nativeAdm), - adomain: ['example.com'], - crid: 'creative-id', - exp: 300, - nurl: 'https://example.com/win' + const nativeAdm = { + ver: '1.2', + assets: [ + {id: 1, title: {text: 'Native Ad Title'}}, + {id: 2, img: {url: 'https://example.com/image.jpg', w: 300, h: 250}} + ], + link: { + url: 'https://example.com/click' + } + }; + + const serverResponse = { + body: { + id: 'response-id', + seatbid: [{ + bid: [{ + id: 'bid-id', + impid: request.data.imp[0].id, + price: 1.5, + adm: JSON.stringify(nativeAdm), + adomain: ['example.com'], + crid: 'creative-id', + exp: 300, + nurl: 'https://example.com/win' + }], + seat: 'taboola' }], - seat: 'taboola' - }], - cur: 'USD' - } - }; + cur: 'USD' + } + }; - const res = spec.interpretResponse(serverResponse, request); + const res = spec.interpretResponse(serverResponse, request); - expect(res).to.be.an('array').with.lengthOf(1); - expect(res[0].mediaType).to.equal('native'); - expect(res[0].native).to.exist; - expect(res[0].native.ortb).to.deep.equal(nativeAdm); - expect(res[0]).to.not.have.property('ad'); - }); + expect(res).to.be.an('array').with.lengthOf(1); + expect(res[0].mediaType).to.equal('native'); + expect(res[0].native).to.exist; + expect(res[0].native.ortb).to.deep.equal(nativeAdm); + expect(res[0]).to.not.have.property('ad'); + }); + } }); }); }) From 3f7d7aeb4d1abf7c6a95f8b4f8b8cb58c37b0d13 Mon Sep 17 00:00:00 2001 From: Roni Shefi Date: Wed, 14 Jan 2026 15:57:32 +0200 Subject: [PATCH 13/15] updated media type checks --- modules/taboolaBidAdapter.js | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index d6da9f3fc50..d76b366c5e7 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -183,9 +183,8 @@ const converter = ortbConverter({ return reqData; }, bidResponse(buildBidResponse, bid, context) { - const hasNative = !!context.bidRequest?.mediaTypes?.native; - const hasBanner = !!context.bidRequest?.mediaTypes?.banner; - context.mediaType = hasNative && !hasBanner ? NATIVE : BANNER; + const { mediaType } = getMediaType(context.bidRequest); + context.mediaType = mediaType; if (context.mediaType === NATIVE) { const admObj = safeJSONParse(bid.adm); @@ -220,8 +219,7 @@ export const spec = { if (!hasPublisherAndTag) { return false; } - const hasBanner = !!bidRequest.mediaTypes?.banner; - const hasNative = !!bidRequest.mediaTypes?.native; + const { hasBanner, hasNative } = getMediaType(bidRequest); return hasBanner || hasNative; }, buildRequests: (validBidRequests, bidderRequest) => { @@ -233,8 +231,8 @@ export const spec = { context: { auctionId } }); const {publisherId} = bidRequest.params; - const isNative = !!bidRequest.mediaTypes?.native; - const baseUrl = isNative ? NATIVE_ENDPOINT_URL : BANNER_ENDPOINT_URL; + const { mediaType } = getMediaType(bidRequest); + const baseUrl = mediaType === NATIVE ? NATIVE_ENDPOINT_URL : BANNER_ENDPOINT_URL; const url = baseUrl + '?publisher=' + publisherId; return { @@ -453,15 +451,16 @@ function fillTaboolaReqData(bidderRequest, bidRequest, data, context) { function fillTaboolaImpData(bid, imp) { const {tagId, position} = bid.params; - const bannerSizes = bid.mediaTypes?.banner?.sizes; - if (bannerSizes) { - imp.banner = getBanners(bannerSizes, position); + const { mediaType, hasBanner } = getMediaType(bid); + if (hasBanner) { + imp.banner = getBanners(bid.mediaTypes.banner.sizes, position); } - imp.tagid = tagId; + imp.tagid = tagId; if (typeof bid.getFloor === 'function') { const floorInfo = bid.getFloor({ currency: CURRENCY, + mediaType: mediaType, size: '*' }); if (isPlainObject(floorInfo) && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { @@ -517,4 +516,14 @@ function getSizes(sizes) { } } +function getMediaType(bidRequest) { + const hasBanner = !!bidRequest?.mediaTypes?.banner?.sizes; + const hasNative = !!bidRequest?.mediaTypes?.native; + return { + hasBanner, + hasNative, + mediaType: hasNative && !hasBanner ? NATIVE : BANNER + }; +} + registerBidder(spec); From 15ad62f187fdb72d5d2494c104e28431f1d9279f Mon Sep 17 00:00:00 2001 From: Roni Shefi Date: Wed, 14 Jan 2026 16:08:24 +0200 Subject: [PATCH 14/15] added missing tab --- .github/workflows/jscpd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/jscpd.yml b/.github/workflows/jscpd.yml index c3021b2ced7..774493a7d04 100644 --- a/.github/workflows/jscpd.yml +++ b/.github/workflows/jscpd.yml @@ -36,7 +36,7 @@ jobs: ], "output": "./", "pattern": "**/*.js", - "ignore": ["**/*spec.js"] + "ignore": ["**/*spec.js"] }' > .jscpd.json - name: Run jscpd on entire codebase From 4cad25d142d6b7e4d51be82335b31a180ad24189 Mon Sep 17 00:00:00 2001 From: Roni Shefi Date: Mon, 2 Feb 2026 15:40:17 +0200 Subject: [PATCH 15/15] removed tab --- .github/workflows/jscpd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/jscpd.yml b/.github/workflows/jscpd.yml index 774493a7d04..c3021b2ced7 100644 --- a/.github/workflows/jscpd.yml +++ b/.github/workflows/jscpd.yml @@ -36,7 +36,7 @@ jobs: ], "output": "./", "pattern": "**/*.js", - "ignore": ["**/*spec.js"] + "ignore": ["**/*spec.js"] }' > .jscpd.json - name: Run jscpd on entire codebase