-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Teza Bid Adapter: initial release #14093
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0f71aad
936d7c3
0896359
8cba424
117654d
6423a40
328dea9
b1a3b5e
a0294bc
db29cbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| // modules/tezaBidAdapter.js | ||
| // Minimal banner-only adapter for OpenRTB 2.x endpoint | ||
| // npm i -g gulp-cli # one time command | ||
| // npm ci # one time command | ||
| // Build: | ||
| // `gulp build --modules=tezaBidAdapter` | ||
| // Compiled output: ./build/dist/prebid.js | ||
|
|
||
| // modules/tezaBidAdapter.js | ||
| import { registerBidder } from '../src/adapters/bidderFactory.js'; | ||
| import { BANNER } from '../src/mediaTypes.js'; | ||
| import { config } from '../src/config.js'; | ||
|
|
||
| const BIDDER_CODE = 'teza'; | ||
| const ENDPOINT = 'https://dsp-us-east-1-nyc.tezatags.com/openrtb2/auction'; | ||
|
|
||
| function sizesToFormat(sizes) { | ||
| return (sizes || []).map(s => ({ w: s[0], h: s[1] })); | ||
| } | ||
|
|
||
| export const spec = { | ||
| code: BIDDER_CODE, | ||
| supportedMediaTypes: [BANNER], | ||
|
|
||
| isBidRequestValid(bid) { | ||
| return !!(bid?.params?.account); | ||
| }, | ||
|
|
||
| buildRequests(validBidRequests, bidderRequest) { | ||
| if (!validBidRequests?.length) return []; | ||
|
|
||
| const b0 = validBidRequests[0]; | ||
| const account = b0.params.account; | ||
| const test = b0.params.test ? 1 : 0; | ||
|
|
||
| const ortb2 = bidderRequest?.ortb2 || config.getConfig('ortb2') || {}; | ||
| const eids = b0.userIdAsEids || []; | ||
| const schain = b0.schain || null; | ||
| const ri = bidderRequest?.refererInfo || {}; | ||
|
|
||
| const imps = validBidRequests.map(bid => { | ||
| const sizes = bid.mediaTypes?.banner?.sizes || bid.sizes || []; | ||
| const tagid = bid.params.tagid || bid.ortb2Imp?.tagid || bid.ortb2Imp?.ext?.gpid || bid.adUnitCode; | ||
| return { | ||
| id: bid.bidId, | ||
| tagid: tagid, | ||
| secure: 1, | ||
| banner: { format: sizesToFormat(sizes) }, | ||
| bidfloor: bid.params.bidfloor || 0.01, | ||
| bidfloorcur: bid.params.bidfloorcur || 'USD' | ||
| }; | ||
| }); | ||
|
|
||
| const regs = { | ||
| coppa: bidderRequest?.coppa ? 1 : 0, | ||
| ext: { | ||
| gdpr: bidderRequest?.gdprConsent?.gdprApplies ? 1 : 0, | ||
| us_privacy: bidderRequest?.uspConsent || undefined | ||
| }, | ||
| gpp: bidderRequest?.gppConsent?.gppString, | ||
| gpp_sid: bidderRequest?.gppConsent?.applicableSections | ||
| }; | ||
|
|
||
| const user = { | ||
| ...ortb2.user, | ||
| ext: { | ||
| ...(ortb2.user?.ext || {}), | ||
| consent: bidderRequest?.gdprConsent?.consentString, | ||
| eids | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Writing Useful? React with 👍 / 👎. |
||
| } | ||
| }; | ||
|
|
||
| const device = { | ||
| ...ortb2.device, | ||
| ua: (typeof navigator !== 'undefined' ? navigator.userAgent : ''), | ||
| language: (typeof navigator !== 'undefined' ? navigator.language : undefined), | ||
| dnt: (typeof navigator !== 'undefined' && navigator.doNotTrack === '1') ? 1 : 0 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This assignment unconditionally recomputes Useful? React with 👍 / 👎.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. navigator.doNotTrack is deprecated by w3c, you should not be calling it |
||
| }; | ||
|
|
||
| const ortb = { | ||
| id: bidderRequest?.auctionId || String(Date.now()), | ||
| imp: imps, | ||
| at: 1, | ||
| tmax: bidderRequest?.timeout || 1000, | ||
| cur: ['USD'], | ||
| test, | ||
| site: { | ||
| ...ortb2.site, | ||
| domain: ri.domain || (typeof location !== 'undefined' ? location.hostname : ''), | ||
| page: ri.page || (typeof location !== 'undefined' ? location.href : '') | ||
| }, | ||
| device, | ||
| user, | ||
| regs, | ||
| source: schain ? { ext: { schain } } : undefined | ||
| }; | ||
|
|
||
| const url = `${ENDPOINT}?test=${test}&account=${encodeURIComponent(account)}`; | ||
| return { method: 'POST', url, data: ortb }; | ||
| }, | ||
|
|
||
| interpretResponse(serverResponse) { | ||
| const res = serverResponse?.body || {}; | ||
| const cur = Array.isArray(res.cur) ? (res.cur[0] || 'USD') : (res.cur || 'USD'); | ||
| const out = []; | ||
|
|
||
| (res.seatbid || []).forEach(sb => { | ||
| (sb.bid || []).forEach(b => { | ||
| out.push({ | ||
| requestId: b.impid, | ||
| cpm: b.price || 0, | ||
| currency: cur, | ||
| width: b.w, | ||
| height: b.h, | ||
| creativeId: b.crid || b.id || 'teza-crid', | ||
| ttl: 30, | ||
| netRevenue: true, | ||
| ad: b.adm, | ||
| nurl: b.nurl, | ||
| burl: b.burl, | ||
| meta: { advertiserDomains: b.adomain || [] } | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| return out; | ||
| }, | ||
|
|
||
| getUserSyncs() { return []; }, | ||
|
|
||
| onBidWon(bid) { | ||
| let url = bid.burl || bid.nurl || ''; | ||
| if (!url) return; | ||
|
|
||
| try { | ||
| url = decodeURIComponent(url); | ||
| } catch { /* ignore malformed encodings */ } | ||
|
|
||
| url = url.replace(/\$\{AUCTION_PRICE\}/g, String(bid.cpm)); | ||
| new Image().src = url; | ||
| } | ||
| }; | ||
|
|
||
| registerBidder(spec); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # Overview | ||
|
|
||
| ``` | ||
| Module Name: Teza Bidder Adapter | ||
| Module Type: Bidder Adapter | ||
| ``` | ||
|
|
||
| # Description | ||
|
|
||
| Minimal banner-only adapter for an OpenRTB 2.x endpoint. | ||
|
|
||
| ### Bid params | ||
|
|
||
| | Name | Scope | Description | Example | Type | | ||
| | ------------- | -------- | --------------------------------------------------------------------------- | ---------- | --------- | | ||
| | `account` | required | Account identifier provided by Teza. | `acct123` | `string` | | ||
| | `tagid` | optional | Ad placement identifier; falls back to GPID or `adUnitCode` if not present. | `home-top` | `string` | | ||
| | `bidfloor` | optional | Minimum price to bid. Default `0.01`. | `0.10` | `number` | | ||
| | `bidfloorcur` | optional | Currency for `bidfloor`. Default `USD`. | `USD` | `string` | | ||
| | `test` | optional | When `true`, enables test mode (`test=1`) on requests. | `true` | `boolean` | | ||
|
|
||
| # Test Parameters | ||
|
|
||
| ```js | ||
| var adUnits = [ | ||
| { | ||
| code: "div-1", | ||
| mediaTypes: { banner: { sizes: [[300, 250]] } }, | ||
| bids: [ | ||
| { | ||
| bidder: "teza", | ||
| params: { | ||
| account: "acct123", | ||
| bidfloor: 0.1, | ||
| test: true, | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| ]; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| // test/spec/modules/tezaBidAdapter_spec.js | ||
| import { expect } from 'chai'; | ||
| import { spec } from 'modules/tezaBidAdapter.js'; | ||
|
|
||
| describe('tezaBidAdapter', function () { | ||
| const bidderRequest = { | ||
| auctionId: 'auc-1', | ||
| timeout: 1200, | ||
| refererInfo: { | ||
| domain: 'localhost', | ||
| page: 'http://localhost/test-teza.html', | ||
| }, | ||
| gdprConsent: { gdprApplies: true, consentString: 'CONSENT' }, | ||
| uspConsent: '1YNN', | ||
| gppConsent: { gppString: 'GPPSTRING', applicableSections: [7] }, | ||
| ortb2: { | ||
| user: { buyeruid: 'u1' }, | ||
| device: { dnt: 0 }, | ||
| site: { cat: ['IAB1'] }, | ||
| }, | ||
| }; | ||
|
|
||
| const validBid = { | ||
| bidder: 'teza', | ||
| bidId: 'bid-1', | ||
| adUnitCode: 'div-1', | ||
| params: { tagid: 'atagid', account: 'acct123', test: true }, | ||
| schain: { ver: '1.0', complete: 1, nodes: [] }, | ||
| userIdAsEids: [{ source: 'uid.example', uids: [{ id: 'abc' }] }], | ||
| mediaTypes: { | ||
| banner: { | ||
| sizes: [ | ||
| [300, 250], | ||
| [320, 50], | ||
| ], | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| describe('isBidRequestValid', function () { | ||
| it('returns true when tagid and account exist', function () { | ||
| expect(spec.isBidRequestValid(validBid)).to.equal(true); | ||
| }); | ||
| it('returns false when missing account', function () { | ||
| const b = { ...validBid, params: { tagid: 'atagid' } }; | ||
| expect(spec.isBidRequestValid(b)).to.equal(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('buildRequests', function () { | ||
| it('builds POST with account & test on query and expected ORTB fields', function () { | ||
| const req = spec.buildRequests([validBid], bidderRequest); | ||
| expect(req.method).to.equal('POST'); | ||
| expect(req.url).to.match( | ||
| /^https?:\/\/[^\s]+openrtb2\/auction\?test=1&account=acct123$/ | ||
| ); | ||
| expect(req.data).to.be.an('object'); | ||
|
|
||
| const ortb = req.data; | ||
| expect(ortb.id).to.equal('auc-1'); | ||
| expect(ortb.cur).to.deep.equal(['USD']); | ||
| expect(ortb.test).to.equal(1); | ||
|
|
||
| // site | ||
| expect(ortb.site.domain).to.equal('localhost'); | ||
| expect(ortb.site.page).to.match(/http:\/\/localhost\/test-teza\.html/); | ||
|
|
||
| // imp | ||
| expect(ortb.imp).to.have.length(1); | ||
| expect(ortb.imp[0].tagid).to.equal('atagid'); | ||
| expect(ortb.imp[0].banner.format).to.deep.equal([ | ||
| { w: 300, h: 250 }, | ||
| { w: 320, h: 50 }, | ||
| ]); | ||
|
|
||
| // user/device/regs | ||
| expect(ortb.device).to.be.an('object'); | ||
| expect(ortb.user.ext.consent).to.equal('CONSENT'); | ||
| expect(ortb.user.ext.eids).to.be.an('array').with.length(1); | ||
| expect(ortb.regs.ext.gdpr).to.equal(1); | ||
| expect(ortb.regs.ext.us_privacy).to.equal('1YNN'); | ||
| expect(ortb.regs.gpp).to.equal('GPPSTRING'); | ||
| expect(ortb.regs.gpp_sid).to.deep.equal([7]); | ||
|
|
||
| // schain | ||
| expect(ortb.source.ext.schain).to.be.an('object'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('interpretResponse', function () { | ||
| it('maps OpenRTB seatbid to Prebid bids', function () { | ||
| const serverResponse = { | ||
| body: { | ||
| id: 'resp-1', | ||
| cur: 'USD', | ||
| seatbid: [ | ||
| { | ||
| seat: 'teza', | ||
| bid: [ | ||
| { | ||
| id: 'b1', | ||
| impid: 'bid-1', | ||
| price: 0.5, | ||
| w: 300, | ||
| h: 250, | ||
| crid: 'cr1', | ||
| adm: '<div>ad</div>', | ||
| adomain: ['example.com'], | ||
| nurl: 'https://dsp/win?price=${AUCTION_PRICE}', | ||
| burl: 'https://dsp/beacon?price=${AUCTION_PRICE}', | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
| }; | ||
| const out = spec.interpretResponse(serverResponse, {}); | ||
| expect(out).to.have.length(1); | ||
| const b = out[0]; | ||
| expect(b.requestId).to.equal('bid-1'); | ||
| expect(b.cpm).to.equal(0.5); | ||
| expect(b.width).to.equal(300); | ||
| expect(b.height).to.equal(250); | ||
| expect(b.creativeId).to.equal('cr1'); | ||
| expect(b.ad).to.match(/<div>ad<\/div>/); | ||
| expect(b.meta.advertiserDomains).to.deep.equal(['example.com']); | ||
| expect(b.nurl).to.be.a('string'); | ||
| expect(b.burl).to.be.a('string'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('onBidWon', function () { | ||
| let OriginalImage; | ||
| let fired; | ||
|
|
||
| beforeEach(function () { | ||
| fired = []; | ||
| OriginalImage = global.Image; | ||
|
|
||
| // Mock with getter+setter to satisfy eslint accessor-pairs | ||
| global.Image = class { | ||
| constructor() { | ||
| this._src = ''; | ||
| } | ||
| get src() { | ||
| return this._src; | ||
| } | ||
| set src(url) { | ||
| this._src = url; | ||
| fired.push(url); | ||
| } | ||
| }; | ||
| }); | ||
|
|
||
| afterEach(function () { | ||
| global.Image = OriginalImage; | ||
| }); | ||
|
|
||
| it('pings burl (falls back to nurl) with cleared AUCTION_PRICE macro', function () { | ||
| const bid = { | ||
| burl: 'https://dsp/beacon?price=${AUCTION_PRICE}', | ||
| nurl: 'https://dsp/win?price=${AUCTION_PRICE}', | ||
| cpm: 1.23, | ||
| }; | ||
| spec.onBidWon(bid); | ||
| expect(fired).to.have.length(1); | ||
| expect(fired[0]).to.equal('https://dsp/beacon?price=1.23'); | ||
| }); | ||
|
|
||
| it('uses nurl when burl absent', function () { | ||
| const bid = { nurl: 'https://dsp/win?price=${AUCTION_PRICE}', cpm: 0.5 }; | ||
| spec.onBidWon(bid); | ||
| expect(fired[0]).to.equal('https://dsp/win?price=0.5'); | ||
| }); | ||
|
|
||
| it('does nothing when neither url is present', function () { | ||
| const bid = { cpm: 0.5 }; | ||
| spec.onBidWon(bid); | ||
| expect(fired).to.have.length(0); | ||
| }); | ||
|
|
||
| describe('alias', function () { | ||
| it('works under alias', function () { | ||
| const vb = { ...validBid, bidder: 'tezaAlias' }; // reuse scoped validBid | ||
| const req = spec.buildRequests([vb], bidderRequest); | ||
| expect(req.url).to.match(/openrtb2\/auction\?test=1&account=acct123$/); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The adapter sets
imp.bidflooronly frombid.params.bidfloor(or a hardcoded default) and never callsbid.getFloor(), so floor rules from the Floors module are ignored. In auctions where publishers enable dynamic floors, this will send stale/too-low floors to Teza and can materially change win rates and revenue behavior.Useful? React with 👍 / 👎.