-
Notifications
You must be signed in to change notification settings - Fork 2.3k
pubstackBidAdapter: initial release #14490
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
base: master
Are you sure you want to change the base?
Changes from all commits
2f87984
bb4d729
c75ced9
17ec281
6bc82fe
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,50 @@ | ||
| import { canAccessWindowTop, getWindowSelf, getWindowTop } from "../../src/utils.js"; | ||
| import { getBoundingClientRect } from "../boundingClientRect/boundingClientRect.js"; | ||
| import { getGptSlotInfoForAdUnitCode } from "../gptUtils/gptUtils.js"; | ||
|
|
||
| export const getElementForAdUnitCode = (adUnitCode: string): HTMLElement | undefined => { | ||
| if (!adUnitCode) return; | ||
| const win = canAccessWindowTop() ? getWindowTop() : getWindowSelf(); | ||
| const doc = win.document; | ||
| let element = doc?.getElementById(adUnitCode); | ||
| if (element) return element; | ||
| const divId = getGptSlotInfoForAdUnitCode(adUnitCode)?.divId; | ||
| element = doc?.getElementById(divId); | ||
| if (element) return element; | ||
| }; | ||
|
|
||
| export const getViewportDistance = (adUnitCode?: string): number | undefined => { | ||
| try { | ||
| const round = (value: number) => Number(value.toFixed(2)); | ||
| const element = getElementForAdUnitCode(adUnitCode); | ||
| if (!element) return; | ||
| const rect = getBoundingClientRect(element); | ||
| if (!rect) return; | ||
|
|
||
| const win = canAccessWindowTop() ? getWindowTop() : getWindowSelf(); | ||
| const doc = win.document; | ||
|
|
||
| const viewportHeight = win.innerHeight || | ||
| doc?.documentElement?.clientHeight || | ||
| doc?.body?.clientHeight || | ||
| 0; | ||
|
|
||
| if (!viewportHeight) return; | ||
|
|
||
| if (rect.top > viewportHeight) { | ||
| return round((rect.top - viewportHeight) / viewportHeight); | ||
| } | ||
| if (rect.bottom < 0) { | ||
| return round(rect.bottom / viewportHeight); | ||
| } | ||
| if (rect.top < 0) { | ||
| return round(rect.top / viewportHeight); | ||
| } | ||
| if (rect.bottom > viewportHeight) { | ||
| return round((rect.bottom - viewportHeight) / viewportHeight); | ||
| } | ||
| return 0; | ||
| } catch (_) {} | ||
| }; | ||
|
|
||
| export const isPageVisible = (): boolean => document.visibilityState === "visible"; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # Overview | ||
| ``` | ||
| Module Name: Pubstack Bidder Adapter | ||
| Module Type: Bidder Adapter | ||
| Maintainer: prebid@pubstack.io | ||
| ``` | ||
|
|
||
| # Description | ||
| Connects to Pubstack exchange for bids. | ||
|
|
||
| Pubstack bid adapter supports all media type including video, banner and native. | ||
|
|
||
| # Test Parameters | ||
| ``` | ||
| var adUnits = [{ | ||
| code: 'adunit-1', | ||
| mediaTypes: { | ||
| banner: { | ||
| sizes: [[300, 250]] | ||
| } | ||
| }, | ||
| bids: [{ | ||
| bidder: 'pubstack', | ||
| params: { | ||
| siteId: 'your-site-id', | ||
| adUnitName: 'adunit-1' | ||
| } | ||
| }] | ||
| }]; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import { deepSetValue, logError } from '../src/utils.js'; | ||
| import { AdapterRequest, BidderSpec, registerBidder, ServerResponse } from '../src/adapters/bidderFactory.js'; | ||
| import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; | ||
| import { ortbConverter } from '../libraries/ortbConverter/converter.js'; | ||
| import { getElementForAdUnitCode, getViewportDistance, isPageVisible } from '../libraries/pubstackUtils/index.js'; | ||
| import { BidRequest, ClientBidderRequest } from '../src/adapterManager.js'; | ||
| import { ORTBRequest } from '../src/prebid.public.js'; | ||
| import { config } from '../src/config.js'; | ||
| import { SyncType } from '../src/userSync.js'; | ||
| import { ConsentData, CONSENT_GDPR, CONSENT_USP, CONSENT_GPP } from '../src/consentHandler.js'; | ||
| import { getGlobal } from '../src/prebidGlobal.js'; | ||
|
|
||
| const BIDDER_CODE = 'pubstack'; | ||
| const GVLID = 1408; | ||
| const REQUEST_URL = 'https://node.pbstck.com/openrtb2/auction'; | ||
| const COOKIESYNC_IFRAME_URL = 'https://cdn.pbstck.com/async_usersync.html'; | ||
| const COOKIESYNC_PIXEL_URL = 'https://cdn.pbstck.com/async_usersync.png'; | ||
|
|
||
| declare module '../src/adUnits' { | ||
| interface BidderParams { | ||
| [BIDDER_CODE]: { | ||
| siteId: string; | ||
| adUnitName: string; | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| type GetUserSyncFn = ( | ||
| syncOptions: { | ||
| iframeEnabled: boolean; | ||
| pixelEnabled: boolean; | ||
| }, | ||
| responses: ServerResponse[], | ||
| gdprConsent: null | ConsentData[typeof CONSENT_GDPR], | ||
| uspConsent: null | ConsentData[typeof CONSENT_USP], | ||
| gppConsent: null | ConsentData[typeof CONSENT_GPP]) => ({ type: SyncType, url: string })[] | ||
|
|
||
| const siteIds: Set<string> = new Set(); | ||
| let cntRequest = 0; | ||
| let cntImp = 0; | ||
| const uStart = performance.now(); | ||
|
|
||
| const converter = ortbConverter({ | ||
| imp(buildImp, bidRequest: BidRequest<typeof BIDDER_CODE>, context) { | ||
| cntImp++; | ||
| const imp = buildImp(bidRequest, context); | ||
| deepSetValue(imp, `ext.prebid.bidder.${BIDDER_CODE}.adUnitName`, bidRequest.params.adUnitName); | ||
| deepSetValue(imp, `ext.prebid.bidder.${BIDDER_CODE}.adUnitCode`, bidRequest.adUnitCode); | ||
| deepSetValue(imp, `ext.prebid.bidder.${BIDDER_CODE}.divId`, getElementForAdUnitCode(bidRequest.adUnitCode)?.id); | ||
| deepSetValue(imp, `ext.prebid.bidder.${BIDDER_CODE}.vpl`, getViewportDistance(bidRequest.adUnitCode)); | ||
| return imp; | ||
| }, | ||
| request(buildRequest, imps, bidderRequest, context) { | ||
| cntRequest++; | ||
| const request = buildRequest(imps, bidderRequest, context); | ||
| const siteId = bidderRequest.bids[0].params.siteId; | ||
| siteIds.add(siteId); | ||
| deepSetValue(request, 'site.publisher.id', siteId); | ||
| deepSetValue(request, 'test', config.getConfig('debug') ? 1 : 0); | ||
| deepSetValue(request, 'ext.prebid.version', getGlobal()?.version ?? 'unknown'); | ||
| deepSetValue(request, `ext.prebid.cntRequest`, cntRequest); | ||
| deepSetValue(request, `ext.prebid.cntImp`, cntImp); | ||
| deepSetValue(request, `ext.prebid.pVisible`, isPageVisible()); | ||
| deepSetValue(request, `ext.prebid.uStart`, Math.trunc((performance.now() - uStart) / 1000)); | ||
| return request; | ||
| }, | ||
| }); | ||
|
|
||
| const isBidRequestValid = (bid: BidRequest<typeof BIDDER_CODE>): boolean => { | ||
| if (!bid.params.siteId || typeof bid.params.siteId !== 'string') { | ||
| logError('bid.params.siteId needs to be a string'); | ||
| if (config.getConfig('debug') === false) return false; | ||
| } | ||
| if (!bid.params.adUnitName || typeof bid.params.adUnitName !== 'string') { | ||
| logError('bid.params.adUnitName needs to be a string'); | ||
| if (config.getConfig('debug') === false) return false; | ||
| } | ||
| return true; | ||
| }; | ||
|
|
||
| const buildRequests = ( | ||
| bidRequests: BidRequest<typeof BIDDER_CODE>[], | ||
| bidderRequest: ClientBidderRequest<typeof BIDDER_CODE>, | ||
| ): AdapterRequest => { | ||
| const data: ORTBRequest = converter.toORTB({ bidRequests, bidderRequest }); | ||
| const siteId = data.site.publisher.id; | ||
| return { | ||
| method: 'POST', | ||
| url: `${REQUEST_URL}?siteId=${siteId}`, | ||
| data, | ||
| }; | ||
| }; | ||
|
|
||
| const interpretResponse = (serverResponse, bidRequest) => { | ||
| if (!serverResponse?.body) { | ||
| return []; | ||
| } | ||
| return converter.fromORTB({ request: bidRequest.data, response: serverResponse.body }); | ||
| }; | ||
|
|
||
| const getUserSyncs: GetUserSyncFn = (syncOptions, _serverResponses, gdprConsent, uspConsent, gppConsent) => { | ||
| const isIframeEnabled = syncOptions.iframeEnabled; | ||
| const isPixelEnabled = syncOptions.pixelEnabled; | ||
|
|
||
| if (!isIframeEnabled && !isPixelEnabled) { | ||
| return []; | ||
| } | ||
|
|
||
| const payload = btoa(JSON.stringify({ | ||
| gdprConsentString: gdprConsent?.consentString, | ||
| gdprApplies: gdprConsent?.gdprApplies, | ||
| uspConsent, | ||
| gpp: gppConsent?.gppString, | ||
| gpp_sid: gppConsent?.applicableSections | ||
|
|
||
| })); | ||
| const syncUrl = isIframeEnabled ? COOKIESYNC_IFRAME_URL : COOKIESYNC_PIXEL_URL; | ||
|
|
||
| return Array.from(siteIds).map(siteId => ({ | ||
| type: isIframeEnabled ? 'iframe' : 'image', | ||
| url: `${syncUrl}?consent=${payload}&siteId=${siteId}`, | ||
|
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.
The sync URL concatenates raw Base64 into Useful? React with 👍 / 👎. |
||
| })); | ||
| }; | ||
|
|
||
| export const spec: BidderSpec<typeof BIDDER_CODE> = { | ||
| code: BIDDER_CODE, | ||
| aliases: [{code: `${BIDDER_CODE}_server`, gvlid: GVLID}], | ||
| gvlid: GVLID, | ||
| supportedMediaTypes: [BANNER, VIDEO, NATIVE], | ||
| isBidRequestValid, | ||
| buildRequests, | ||
| interpretResponse, | ||
| getUserSyncs, | ||
| }; | ||
|
|
||
| registerBidder(spec); | ||
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.
we just added another library for this, let us know if you can use that or if it needs modifications