Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions modules/clydoBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { deepSetValue, deepAccess, isFn } from '../src/utils.js';
import { toOrtbNativeRequest } from '../src/native.js';
import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js';
import { ortbConverter } from '../libraries/ortbConverter/converter.js';

const BIDDER_CODE = 'clydo';
const METHOD = 'POST';
const params = {
region: "{{region}}",
partnerId: "{{partnerId}}"
}
const BASE_ENDPOINT_URL = `https://${params.region}.clydo.io/${params.partnerId}`

const converter = ortbConverter({
context: {
netRevenue: true,
ttl: 30
}
});

export const spec = {
code: BIDDER_CODE,
supportedMediaTypes: [BANNER, VIDEO, NATIVE],
userSync: {
topics: false,
},
isBidRequestValid: function(bid) {
if (!bid || !bid.params) return false;
const { partnerId, region } = bid.params;
if (typeof partnerId !== 'string' || partnerId.length === 0) return false;
if (typeof region !== 'string') return false;
const allowedRegions = ['us', 'usw', 'eu', 'apac'];
return allowedRegions.includes(region);
},
buildRequests: function(validBidRequests, bidderRequest) {
const data = converter.toORTB({bidRequests: validBidRequests, bidderRequest});
const { partnerId, region } = validBidRequests[0].params;

if (Array.isArray(data.imp)) {
data.imp.forEach((imp, index) => {
const srcBid = validBidRequests[index] || validBidRequests[0];
const bidderParams = deepAccess(srcBid, 'params') || {};
deepSetValue(data, `imp.${index}.ext.clydo`, bidderParams);

const mediaTypes = deepAccess(srcBid, 'mediaTypes') || {};
if (mediaTypes.video && !imp.video) {
deepSetValue(data, `imp.${index}.video`, {});
}
if (mediaTypes.native && !imp.native) {
deepSetValue(data, `imp.${index}.native`, {});
}

const mediaType = imp.banner ? 'banner' : (imp.video ? 'video' : (imp.native ? 'native' : '*'));
let floor = deepAccess(srcBid, 'params.floor');
if (!floor && isFn(srcBid.getFloor)) {
const floorInfo = srcBid.getFloor({currency: 'USD', mediaType, size: '*'});
if (floorInfo && typeof floorInfo.floor === 'number') {
floor = floorInfo.floor;
}
}
if (typeof floor === 'number') {
deepSetValue(data, `imp.${index}.bidfloor`, floor);
deepSetValue(data, `imp.${index}.bidfloorcur`, 'USD');
}

if (imp.native && !imp.native.request) {
const nativeParams = srcBid.nativeParams || deepAccess(srcBid, 'mediaTypes.native');
if (nativeParams) {
const ortbNative = toOrtbNativeRequest(nativeParams);
if (ortbNative) {
deepSetValue(data, `imp.${index}.native.request`, JSON.stringify(ortbNative));
deepSetValue(data, `imp.${index}.native.ver`, '1.2');
}
}
}
});
}

const schain = deepAccess(validBidRequests, '0.schain');
if (schain) {
deepSetValue(data, 'source.ext.schain', schain);
}

const eids = deepAccess(validBidRequests, '0.userIdAsEids');
if (Array.isArray(eids)) {
deepSetValue(data, 'user.ext.eids', eids);
}

if (bidderRequest && bidderRequest.gdprConsent) {
deepSetValue(data, 'user.ext.consent', bidderRequest.gdprConsent.consentString);
deepSetValue(data, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0);
}
if (bidderRequest && typeof bidderRequest.uspConsent === 'string') {
deepSetValue(data, 'regs.ext.us_privacy', bidderRequest.uspConsent);
}
if (bidderRequest && bidderRequest.gppConsent) {
deepSetValue(data, 'regs.gpp', bidderRequest.gppConsent.gppString);
deepSetValue(data, 'regs.gpp_sid', bidderRequest.gppConsent.applicableSections);
}
const ENDPOINT_URL = BASE_ENDPOINT_URL
.replace(params.partnerId, partnerId)
.replace(params.region, region);

return [{
method: METHOD,
url: ENDPOINT_URL,
data
}]
},
interpretResponse: function(serverResponse, request) {
let bids = [];
let body = serverResponse.body || {};
if (body) {
const normalized = Array.isArray(body.seatbid)
? {
...body,
seatbid: body.seatbid.map(seat => ({
...seat,
bid: (seat.bid || []).map(b => {
if (typeof b?.adm === 'string') {
try {
const parsed = JSON.parse(b.adm);
if (parsed && parsed.native && Array.isArray(parsed.native.assets)) {
return {...b, adm: JSON.stringify(parsed.native)};
}
} catch (e) {}
}
return b;
})
}))
}
: body;
bids = converter.fromORTB({response: normalized, request: request.data}).bids;
}
return bids;
},
}
registerBidder(spec);
93 changes: 93 additions & 0 deletions modules/clydoBidAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Overview

```
Module Name: Clydo Bid Adapter
Module Type: Bidder Adapter
Maintainer: [email protected]
```

# Description

The Clydo adapter connects to the Clydo bidding endpoint to request bids using OpenRTB.

- Supported media types: banner, video, native
- Endpoint is derived from parameters: `https://{region}.clydo.io/{partnerId}`
- Passes GDPR, USP/CCPA, and GPP consent when available
- Propagates `schain` and `userIdAsEids`

# Bid Params

- `partnerId` (string, required): Partner identifier provided by Clydo
- `region` (string, required): One of `us`, `usw`, `eu`, `apac`

# Test Parameters (Banner)
```javascript
var adUnits = [{
code: '/15185185/prebid_banner_example_1',
mediaTypes: {
banner: {
sizes: [[300, 250], [300, 600]]
}
},
bids: [{
bidder: 'clydo',
params: {
partnerId: 'abcdefghij',
region: 'us'
}
}]
}];
```

# Test Parameters (Video)
```javascript
var adUnits = [{
code: '/15185185/prebid_video_example_1',
mediaTypes: {
video: {
context: 'instream',
playerSize: [[640, 480]],
mimes: ['video/mp4']
}
},
bids: [{
bidder: 'clydo',
params: {
partnerId: 'abcdefghij',
region: 'us'
}
}]
}];
```

# Test Parameters (Native)
```javascript
var adUnits = [{
code: '/15185185/prebid_native_example_1',
mediaTypes: {
native: {
title: { required: true },
image: { required: true, sizes: [120, 120] },
icon: { required: false, sizes: [50, 50] },
body: { required: false },
sponsoredBy: { required: false },
clickUrl: { required: false },
cta: { required: false }
}
},
bids: [{
bidder: 'clydo',
params: {
partnerId: 'abcdefghij',
region: 'us'
}
}]
}];
```

# Notes

- Floors: If the ad unit implements `getFloor`, the adapter forwards the value as `imp.bidfloor` (USD).
- Consent: When present, the adapter forwards `gdprApplies`/`consentString`, `uspConsent`, and `gpp`/`gpp_sid`.
- Supply Chain and IDs: `schain` is set under `source.ext.schain`; user IDs are forwarded under `user.ext.eids`.

113 changes: 113 additions & 0 deletions test/spec/modules/clydoBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { expect } from 'chai';
import { spec } from 'modules/clydoBidAdapter.js';

function makeBid(overrides = {}) {
return Object.assign({
adUnitCode: '/15185185/prebid_example_1',
bidId: 'bid-1',
ortb2: {},
ortb2Imp: {},
mediaTypes: {
banner: { sizes: [[300, 250]] }
},
bidder: 'clydo',
userIdAsEids: [],
schain: { ver: '1.0' },
params: {
partnerId: 'abcdefghij',
region: 'us'
}
}, overrides);
}

describe('clydoBidAdapter', () => {
describe('isBidRequestValid', () => {
it('returns false for missing params', () => {
expect(spec.isBidRequestValid(makeBid({ params: undefined }))).to.equal(false);
});
it('returns false for invalid region', () => {
expect(spec.isBidRequestValid(makeBid({ params: { partnerId: 'x', region: 'xx' } }))).to.equal(false);
});
it('returns true for valid params', () => {
expect(spec.isBidRequestValid(makeBid())).to.equal(true);
});
});

describe('buildRequests', () => {
it('builds POST request with endpoint and JSON content type', () => {
const bid = makeBid();
const reqs = spec.buildRequests([bid], {});
expect(reqs).to.be.an('array').with.lengthOf(1);
const req = reqs[0];
expect(req.method).to.equal('POST');
expect(req.url).to.include('us');
expect(req.url).to.include('abcdefghij');
expect(req).to.not.have.property('options');
expect(req).to.have.property('data');
});

it('adds imp.ext.clydo and bidfloor when available', () => {
const bid = makeBid({
getFloor: ({ currency }) => ({ floor: 1.5, currency })
});
const req = spec.buildRequests([bid], {})[0];
const data = req.data;
expect(data.imp[0].ext.clydo).to.deep.equal(bid.params);
expect(data.imp[0].bidfloor).to.equal(1.5);
expect(data.imp[0].bidfloorcur).to.equal('USD');
});

describe('banner', () => {
it('builds banner imp when mediaTypes.banner present', () => {
const bid = makeBid({ mediaTypes: { banner: { sizes: [[300, 250]] } } });
const data = spec.buildRequests([bid], {})[0].data;
expect(data.imp[0]).to.have.property('banner');
});
});

describe('video', () => {
it('builds video imp when mediaTypes.video present', () => {
const bid = makeBid({ mediaTypes: { video: { playerSize: [[640, 480]], context: 'instream', mimes: ['video/mp4'] } } });
const data = spec.buildRequests([bid], {})[0].data;
expect(data.imp[0]).to.have.property('video');
});
});

describe('native', () => {
it('builds native imp request when mediaTypes.native present', () => {
const bid = makeBid({ mediaTypes: { native: { title: { required: true }, image: { required: true, sizes: [120, 120] } } } });
const data = spec.buildRequests([bid], {})[0].data;
expect(data.imp[0]).to.have.property('native');
expect(data.imp[0].native).to.have.property('request');
});
});

it('propagates schain and eids and consent', () => {
const bid = makeBid({
userIdAsEids: [{ source: 'test', uids: [{ id: 'abc', atype: 1 }] }]
});
const bidderRequest = {
gdprConsent: { gdprApplies: 1, consentString: 'CONSENT' },
uspConsent: '1YA-',
gppConsent: { gppString: 'GPP', applicableSections: [7] }
};
const data = spec.buildRequests([bid], bidderRequest)[0].data;
expect(data.source.ext.schain).to.deep.equal(bid.schain);
expect(data.user.ext.eids).to.deep.equal(bid.userIdAsEids);
expect(data.user.ext.consent).to.equal('CONSENT');
expect(data.regs.ext.gdpr).to.equal(1);
expect(data.regs.ext.us_privacy).to.equal('1YA-');
expect(data.regs.gpp).to.equal('GPP');
expect(data.regs.gpp_sid).to.deep.equal([7]);
});
});

describe('interpretResponse', () => {
it('returns empty when body is null', () => {
const bid = makeBid();
const req = spec.buildRequests([bid], {})[0];
const res = spec.interpretResponse({ body: null }, req);
expect(res).to.be.an('array').that.is.empty;
});
});
});
Loading