Skip to content
144 changes: 144 additions & 0 deletions modules/tezaBidAdapter.js
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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor floors module when setting imp.bidfloor

The adapter sets imp.bidfloor only from bid.params.bidfloor (or a hardcoded default) and never calls bid.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 👍 / 👎.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve ortb2 EIDs when bid-level IDs are absent

Writing user.ext.eids from eids here overwrites any ortb2.user.ext.eids merged just above. If publishers provide EIDs through ORTB2 first-party data and the first bid has no userIdAsEids, the request sends an empty EID list and drops identity signals for the entire auction request.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep caller-provided device.dnt instead of forcing navigator

This assignment unconditionally recomputes device.dnt from navigator.doNotTrack, overriding ortb2.device.dnt that may be set by the caller. In cases where FPD explicitly sets DNT (or navigator is unavailable), the adapter can emit dnt: 0 incorrectly and send the wrong privacy preference downstream.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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);
41 changes: 41 additions & 0 deletions modules/tezaBidAdapter.md
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,
},
},
],
},
];
```
190 changes: 190 additions & 0 deletions test/spec/modules/tezaBidAdapter_spec.js
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$/);
});
});
});
});
Loading