-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Contxtful Bid Adapter : initial release #12256
Merged
ChrisHuie
merged 18 commits into
prebid:master
from
contxtful-technologies:contxtful-bidadapter
Oct 24, 2024
Merged
Changes from 16 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
b7d4c28
feat: contxtful bid adapter
rufiange 298edd3
fix: ajax
rufiange d868433
fix: config, valid bid request
rufiange 1d424c6
fix: config, valid bid request
rufiange 0173c3c
fix: tests
rufiange 23de79f
refactor: construct url
rufiange b604eea
fix: test
rufiange 943b974
fix: test
rufiange 23967ed
fix: space
rufiange 1d9120a
fix: added beacon
rufiange 3eb270e
fix: test
rufiange 06956ff
fix: test
rufiange 5044696
fix: pbjs version
rufiange a81ba86
doc: beacon for ci
rufiange de23cec
doc: spec to trigger ci
rufiange 512a417
doc: log trigger ci
rufiange c490204
Merge branch 'master' of https://github.com/contxtful-technologies/Pr…
rufiange cfd96d8
fix: imports
rufiange File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
import { registerBidder } from '../src/adapters/bidderFactory.js'; | ||
import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; | ||
import { _each, buildUrl, isStr, isEmptyStr, logInfo, logError } from '../src/utils.js'; | ||
import * as ajax from '../src/ajax.js'; | ||
import { config as pbjsConfig } from '../src/config.js'; | ||
import { | ||
isBidRequestValid, | ||
interpretResponse, | ||
getUserSyncs as getUserSyncsLib, | ||
} from '../libraries/teqblazeUtils/bidderUtils.js'; | ||
import {ortbConverter} from '../libraries/ortbConverter/converter.js'; | ||
|
||
// Constants | ||
const BIDDER_CODE = 'contxtful'; | ||
const BIDDER_ENDPOINT = 'prebid.receptivity.io'; | ||
const MONITORING_ENDPOINT = 'monitoring.receptivity.io'; | ||
const DEFAULT_NET_REVENUE = true; | ||
const DEFAULT_TTL = 300; | ||
const PREBID_VERSION = '$prebid.version$'; | ||
|
||
// ORTB conversion | ||
const converter = ortbConverter({ | ||
context: { | ||
netRevenue: DEFAULT_NET_REVENUE, | ||
ttl: DEFAULT_TTL | ||
}, | ||
imp(buildImp, bidRequest, context) { | ||
let imp = buildImp(bidRequest, context); | ||
return imp; | ||
}, | ||
request(buildRequest, imps, bidderRequest, context) { | ||
const reqData = buildRequest(imps, bidderRequest, context); | ||
return reqData; | ||
}, | ||
bidResponse(buildBidResponse, bid, context) { | ||
const bidResponse = buildBidResponse(bid, context); | ||
return bidResponse; | ||
} | ||
}); | ||
|
||
// Get Bid Floor | ||
const _getRequestBidFloor = (mediaTypes, paramsBidFloor, bid) => { | ||
const bidMediaType = Object.keys(mediaTypes)[0] || 'banner'; | ||
const bidFloor = { floor: 0, currency: 'USD' }; | ||
|
||
if (typeof bid.getFloor === 'function') { | ||
const { currency, floor } = bid.getFloor({ | ||
mediaType: bidMediaType, | ||
size: '*' | ||
}); | ||
floor && (bidFloor.floor = floor); | ||
currency && (bidFloor.currency = currency); | ||
} else if (paramsBidFloor) { | ||
bidFloor.floor = paramsBidFloor | ||
} | ||
|
||
return bidFloor; | ||
} | ||
|
||
// Get Parameters from the config. | ||
const extractParameters = (config) => { | ||
const version = config?.contxtful?.version; | ||
if (!isStr(version) || isEmptyStr(version)) { | ||
throw Error(`contxfulBidAdapter: contxtful.version should be a non-empty string`); | ||
} | ||
|
||
const customer = config?.contxtful?.customer; | ||
if (!isStr(customer) || isEmptyStr(customer)) { | ||
throw Error(`contxfulBidAdapter: contxtful.customer should be a non-empty string`); | ||
} | ||
|
||
return { version, customer }; | ||
} | ||
|
||
// Construct the Payload towards the Bidding endpoint | ||
const buildRequests = (validBidRequests = [], bidderRequest = {}) => { | ||
const ortb2 = converter.toORTB({bidderRequest: bidderRequest, bidRequests: validBidRequests}); | ||
|
||
const bidRequests = []; | ||
_each(validBidRequests, bidRequest => { | ||
const { | ||
mediaTypes = {}, | ||
params = {}, | ||
} = bidRequest; | ||
bidRequest.bidFloor = _getRequestBidFloor(mediaTypes, params.bidfloor, bidRequest); | ||
bidRequests.push(bidRequest) | ||
}); | ||
const config = pbjsConfig.getConfig(); | ||
config.pbjsVersion = PREBID_VERSION; | ||
const {version, customer} = extractParameters(config) | ||
const adapterUrl = buildUrl({ | ||
protocol: 'https', | ||
host: BIDDER_ENDPOINT, | ||
pathname: `/${version}/prebid/${customer}/bid`, | ||
}); | ||
|
||
// https://docs.prebid.org/dev-docs/bidder-adaptor.html | ||
let req = { | ||
url: adapterUrl, | ||
method: 'POST', | ||
data: { | ||
ortb2, | ||
bidRequests, | ||
bidderRequest, | ||
config, | ||
}, | ||
}; | ||
|
||
return req; | ||
}; | ||
|
||
// Prepare a sync object compatible with getUserSyncs. | ||
const constructUrl = (userSyncsDefault, userSyncServer) => { | ||
const urlSyncServer = (userSyncServer?.url ?? '').split('?'); | ||
const userSyncUrl = userSyncsDefault?.url || ''; | ||
const baseSyncUrl = urlSyncServer[0] || ''; | ||
|
||
let url = `${baseSyncUrl}${userSyncUrl}`; | ||
|
||
if (urlSyncServer.length > 1) { | ||
const urlParams = urlSyncServer[1]; | ||
url += url.includes('?') ? `&${urlParams}` : `?${urlParams}`; | ||
} | ||
|
||
return { | ||
...userSyncsDefault, | ||
url, | ||
}; | ||
}; | ||
|
||
// Returns the list of user synchronization objects. | ||
const getUserSyncs = (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { | ||
// Get User Sync Defaults from pbjs lib | ||
const userSyncsDefaultLib = getUserSyncsLib('')(syncOptions, null, gdprConsent, uspConsent, gppConsent); | ||
const userSyncsDefault = userSyncsDefaultLib?.find(item => item.url !== undefined); | ||
|
||
// Map Server Responses to User Syncs list | ||
const serverSyncsData = serverResponses?.flatMap(response => response.body || []) | ||
.map(data => data.syncs) | ||
.find(syncs => Array.isArray(syncs) && syncs.length > 0) || []; | ||
const userSyncs = serverSyncsData | ||
.map(sync => constructUrl(userSyncsDefault, sync)) | ||
.filter(Boolean); // Filter out nulls | ||
return userSyncs; | ||
}; | ||
|
||
// Retrieve the sampling rate for events | ||
const getSamplingRate = (bidderConfig, eventType) => { | ||
const entry = Object.entries(bidderConfig?.contxtful?.sampling ?? {}).find(([key, value]) => key.toLowerCase() === eventType.toLowerCase()); | ||
return entry ? entry[1] : 0.001; | ||
}; | ||
|
||
// Handles the logging of events | ||
const logEvent = (eventType, data, options = {}) => { | ||
const { | ||
samplingEnabled = false, | ||
} = options; | ||
|
||
try { | ||
// Log event | ||
logInfo(BIDDER_CODE, `[${eventType}] ${JSON.stringify(data)}`); | ||
|
||
// Get Config | ||
const bidderConfig = pbjsConfig.getConfig(); | ||
const {version, customer} = extractParameters(bidderConfig); | ||
|
||
// Sampled monitoring | ||
if (samplingEnabled) { | ||
const shouldSampleDice = Math.random(); | ||
const samplingRate = getSamplingRate(bidderConfig, eventType); | ||
if (shouldSampleDice >= samplingRate) { | ||
return; // Don't sample | ||
} | ||
} | ||
|
||
const payload = { type: eventType, data }; | ||
const eventUrl = buildUrl({ | ||
protocol: 'https', | ||
host: MONITORING_ENDPOINT, | ||
pathname: `/${version}/prebid/${customer}/log/${eventType}`, | ||
}); | ||
|
||
// Try sending a beacon | ||
if (ajax.sendBeacon(eventUrl, JSON.stringify(payload))) { | ||
logInfo(BIDDER_CODE, `[${eventType}] Logging data sent using Beacon and payload: ${JSON.stringify(data)}`); | ||
} else { | ||
// Fallback to using ajax | ||
ajax.ajax(eventUrl, null, JSON.stringify(payload), { | ||
method: 'POST', | ||
contentType: 'application/json', | ||
withCredentials: true, | ||
}); | ||
logInfo(BIDDER_CODE, `[${eventType}] Logging data sent using Ajax and payload: ${JSON.stringify(data)}`); | ||
} | ||
} catch (error) { | ||
logError(BIDDER_CODE, `Failed to log event: ${eventType}`); | ||
} | ||
}; | ||
|
||
// Bidder public specification | ||
export const spec = { | ||
code: BIDDER_CODE, | ||
supportedMediaTypes: [BANNER, VIDEO, NATIVE], | ||
|
||
isBidRequestValid, | ||
buildRequests, | ||
interpretResponse, | ||
getUserSyncs, | ||
onBidWon: function(bid, options) { logEvent('onBidWon', bid, { samplingEnabled: false, ...options }); }, | ||
onBidBillable: function(bid, options) { logEvent('onBidBillable', bid, { samplingEnabled: false, ...options }); }, | ||
onAdRenderSucceeded: function(bid, options) { logEvent('onAdRenderSucceeded', bid, { samplingEnabled: false, ...options }); }, | ||
onSetTargeting: function(bid, options) { }, | ||
onTimeout: function(timeoutData, options) { logEvent('onTimeout', timeoutData, { samplingEnabled: true, ...options }); }, | ||
onBidderError: function(args, options) { logEvent('onBidderError', args, { samplingEnabled: true, ...options }); }, | ||
}; | ||
|
||
registerBidder(spec); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# Overview | ||
|
||
``` | ||
Module Name: Contxtful Bidder Adapter | ||
Module Type: Bidder Adapter | ||
Maintainer: contact@contxtful.com | ||
``` | ||
|
||
# Description | ||
|
||
The Contxtful Bidder Adapter supports all mediatypes and connects to demand sources for bids. | ||
|
||
# Configuration | ||
## Global Configuration | ||
Contxtful uses the global configuration to store params once instead of duplicating for each ad unit. | ||
Also, enabling user syncing greatly increases match rates and monetization. | ||
Be sure to call `pbjs.setConfig()` only once. | ||
|
||
```javascript | ||
pbjs.setConfig({ | ||
debug: false, | ||
contxtful: { | ||
customer: '<contxtful_provided_id>', // Required | ||
version: '<contxtful_provided_version>', // Required | ||
}, | ||
userSync: { | ||
filterSettings: { | ||
iframe: { | ||
bidders: ['contxtful'], | ||
filter: 'include' | ||
} | ||
} | ||
} | ||
// [...] | ||
}); | ||
``` | ||
|
||
## Bidder Setting | ||
Contxtful leverages local storage for user syncing. | ||
|
||
```javascript | ||
pbjs.bidderSettings = { | ||
contxtful: { | ||
storageAllowed: true | ||
} | ||
} | ||
``` | ||
|
||
# Example Ad-units configuration | ||
```javascript | ||
var adUnits = [ | ||
{ | ||
code: 'adunit1', | ||
mediaTypes: { | ||
banner: { | ||
sizes: [ [300, 250], [320, 50] ], | ||
} | ||
}, | ||
bids: [{ | ||
bidder: 'contxtful', | ||
}] | ||
}, | ||
{ | ||
code: 'addunit2', | ||
mediaTypes: { | ||
video: { | ||
playerSize: [ [640, 480] ], | ||
context: 'instream', | ||
minduration: 5, | ||
maxduration: 60, | ||
} | ||
}, | ||
bids: [{ | ||
bidder: 'contxtful', | ||
}] | ||
}, | ||
{ | ||
code: 'addunit3', | ||
mediaTypes: { | ||
native: { | ||
title: { | ||
required: true | ||
}, | ||
body: { | ||
required: true | ||
}, | ||
icon: { | ||
required: true, | ||
size: [64, 64] | ||
} | ||
} | ||
}, | ||
bids: [{ | ||
bidder: 'contxtful', | ||
}] | ||
} | ||
]; | ||
``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
import what you need please
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.
@patmmccann Thank you for pointing this out! I fixed it in this commit