diff --git a/modules/epomDspBidAdapter.js b/modules/epomDspBidAdapter.js new file mode 100644 index 00000000000..ee4dade4abd --- /dev/null +++ b/modules/epomDspBidAdapter.js @@ -0,0 +1,108 @@ +/** + * @name epomDspBidAdapter + * @version 1.0.0 + * @description Adapter for Epom DSP and AdExchange + * @module modules/epomDspBidAdapter + * @license Open Source - Apache 2.0 + */ + +import { logError, logWarn, registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +const BIDDER_CODE = 'epomDsp'; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid(bid) { + const globalSettings = config.getBidderConfig()[BIDDER_CODE]?.epomSettings || {}; + const hasEndpoint = bid.params?.endpoint || globalSettings.endpoint; + return !!(hasEndpoint); + }, + + buildRequests(bidRequests, bidderRequest) { + const bidderConfig = config.getBidderConfig(); + const globalSettings = bidderConfig['epomDsp']?.epomSettings || {}; + + const requests = bidRequests.map((bid) => { + const endpoint = bid.params?.endpoint || globalSettings.endpoint; + const payload = { + ...bid, + referer: bidderRequest?.refererInfo?.referer, + gdprConsent: bidderRequest?.gdprConsent, + uspConsent: bidderRequest?.uspConsent, + }; + delete payload.params; + + return { + method: 'POST', + url: endpoint, + data: JSON.parse(JSON.stringify(payload)), + options: { + contentType: 'application/json', + withCredentials: false, + }, + }; + }); + + return requests.filter((request) => request !== null); + }, + + interpretResponse(serverResponse) { + const bidResponses = []; + const response = serverResponse.body; + + if (response && Array.isArray(response.bids)) { + response.bids.forEach((bid) => { + if (bid.cpm && bid.ad && bid.width && bid.height) { + bidResponses.push({ + requestId: bid.requestId, + cpm: bid.cpm, + currency: bid.currency, + width: bid.width, + height: bid.height, + ad: bid.ad, + creativeId: bid.creativeId || bid.requestId, + ttl: typeof bid.ttl === 'number' ? bid.ttl : 300, + netRevenue: bid.netRevenue !== false, + }); + } else { + logWarn(`[${BIDDER_CODE}] Invalid bid response:`, bid); + } + }); + } else { + logError(`[${BIDDER_CODE}] Empty or invalid server response:`, serverResponse); + } + + return bidResponses; + }, + + getUserSyncs(syncOptions, serverResponses) { + const syncs = []; + + if (syncOptions.iframeEnabled && serverResponses.length > 0) { + serverResponses.forEach((response) => { + if (response.body?.userSync?.iframe) { + syncs.push({ + type: 'iframe', + url: response.body.userSync.iframe, + }); + } + }); + } + + if (syncOptions.pixelEnabled && serverResponses.length > 0) { + serverResponses.forEach((response) => { + if (response.body?.userSync?.pixel) { + syncs.push({ + type: 'image', + url: response.body.userSync.pixel, + }); + } + }); + } + + return syncs; + }, +}; + +registerBidder(spec); diff --git a/modules/epomDspBidAdapter.md b/modules/epomDspBidAdapter.md new file mode 100644 index 00000000000..29126533b11 --- /dev/null +++ b/modules/epomDspBidAdapter.md @@ -0,0 +1,166 @@ + +# Overview + +``` +Module Name: Epom DSP Bid Adapter +Module Type: Bidder Adapter +Maintainer: support@epom.com +``` + +# Description + +The **Epom DSP Bid Adapter** connects publishers to the Epom DSP Exchange for programmatic advertising. This adapter supports banner formats and follows the OpenRTB protocol. + +# Supported Media Types + +- **Banner** + +--- + +# Integration Guide for Publishers + +## Basic Configuration + +Here is an example configuration for integrating the Epom DSP Bid Adapter into your Prebid.js setup. + +### Sample Banner Ad Unit + +```javascript +var adUnits = [ + { + code: 'epom-banner-div', // Ad slot HTML element ID + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] // Banner sizes + } + }, + bids: [ + { + bidder: 'epomDsp', // Adapter code + params: { + endpoint: 'https://your-epom-endpoint.com/bid', // Epom DSP endpoint + }, + adUnitId: 'sampleAdUnitId123', // Unique Ad Unit ID + bidfloor: 0.5, // Minimum bid floor (optional) + } + ] + } +]; +``` + +--- + +# Params + +Below are the parameters that can be configured in the `params` object for the **Epom DSP Bid Adapter**. + +| Parameter | Type | Required | Description | +|----------------|----------|----------|-----------------------------------------------------------------------------| +| `endpoint` | string | Yes | The URL of the Epom DSP bidding endpoint. | +| `adUnitId` | string | No | Unique identifier for the Ad Unit. | +| `bidfloor` | number | No | Minimum CPM value for the bid in USD. | +| `banner` | object | No | Banner-specific parameters like `btype` (ad type) or `pos` (ad position). | + +--- + +# Global Settings (Optional) + +You can define global configuration parameters for the **Epom DSP Bid Adapter** using `pbjs.setBidderConfig`. These settings will apply to all requests made via the adapter. + +### Example Global Configuration + +```javascript +pbjs.setBidderConfig({ + bidders: ['epomDsp'], + config: { + epomSettings: { + endpoint: 'https://your-epom-endpoint.com/bid', // Epom DSP endpoint + } + } +}); +``` + +--- + +# Response Format + +The **Epom DSP Bid Adapter** complies with the OpenRTB protocol and returns responses in the following format: + +```json +{ + "bids": [ + { + "requestId": "uniqueRequestId", + "cpm": 1.5, + "currency": "USD", + "width": 300, + "height": 250, + "ad": "
Ad Markup
", + "creativeId": "creative123", + "ttl": 300, + "netRevenue": true + } + ] +} +``` + +### Response Fields + +| Field | Type | Description | +|----------------|----------|--------------------------------------------------------------------------| +| `requestId` | string | Unique identifier for the bid request. | +| `cpm` | number | Cost per thousand impressions (CPM) in USD. | +| `currency` | string | Currency of the bid (default: USD). | +| `width` | number | Width of the ad unit in pixels. | +| `height` | number | Height of the ad unit in pixels. | +| `ad` | string | HTML markup for rendering the ad. | +| `creativeId` | string | Identifier for the creative. | +| `ttl` | number | Time-to-live for the bid (in seconds). | +| `netRevenue` | boolean | Indicates whether the CPM is net revenue (`true` by default). | + +--- + +# GDPR and Privacy Compliance + +The **Epom DSP Bid Adapter** supports GDPR and CCPA compliance. Consent information can be passed via the following fields in `bidderRequest`: + +- `bidderRequest.gdprConsent` +- `bidderRequest.uspConsent` + +--- + +# Support + +For questions or issues with integration, please contact [Epom Support](mailto:support@epom.com). + +--- + +# Examples + +## Basic Banner Ad Unit + +```javascript +var adUnits = [ + { + code: 'epom-banner', // Ad slot HTML element ID + mediaTypes: { + banner: { + sizes: [[300, 250], [728, 90]] + } + }, + bids: [ + { + bidder: 'epomDsp', + params: { + endpoint: 'https://your-epom-endpoint.com/bid', + }, + adUnitId: 'adUnit123', + bidfloor: 0.5 + } + ] + } +]; +``` diff --git a/test/spec/modules/epomDspBidAdapter_spec.js b/test/spec/modules/epomDspBidAdapter_spec.js new file mode 100644 index 00000000000..ba051841678 --- /dev/null +++ b/test/spec/modules/epomDspBidAdapter_spec.js @@ -0,0 +1,155 @@ +import { expect } from 'chai'; +import { spec } from 'modules/epomDspBidAdapter.js'; +import { config } from 'src/config.js'; +import { logError, logWarn } from 'src/utils.js'; + +describe('epomDspBidAdapter', () => { + const BIDDER_CODE = 'epomDsp'; + + describe('isBidRequestValid', () => { + it('should return true when bid has endpoint in params', () => { + const bid = { + params: { + endpoint: 'https://example.com' + } + }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('should return false when endpoint is missing', () => { + config.setBidderConfig({}); + const bid = { + params: {} + }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', () => { + it('should build requests correctly', () => { + const bidRequests = [{ + params: { + endpoint: 'https://example.com' + }, + bidId: '123', + auctionId: '456' + }]; + const bidderRequest = { + refererInfo: { + referer: 'https://example.com' + }, + gdprConsent: 'consentString', + uspConsent: 'uspConsentString' + }; + const requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests).to.have.lengthOf(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: 'https://example.com', + data: { + bidId: '123', + auctionId: '456', + referer: 'https://example.com', + gdprConsent: 'consentString', + uspConsent: 'uspConsentString' + }, + options: { + contentType: 'application/json', + withCredentials: false + } + }); + }); + }); + + describe('interpretResponse', () => { + it('should interpret valid server response', () => { + const serverResponse = { + body: { + bids: [{ + requestId: '123', + cpm: 1.23, + currency: 'USD', + width: 300, + height: 250, + ad: '', + creativeId: '456', + ttl: 60, + netRevenue: true + }] + } + }; + const bidResponses = spec.interpretResponse(serverResponse); + expect(bidResponses).to.have.lengthOf(1); + expect(bidResponses[0]).to.deep.equal({ + requestId: '123', + cpm: 1.23, + currency: 'USD', + width: 300, + height: 250, + ad: '', + creativeId: '456', + ttl: 60, + netRevenue: true + }); + }); + }); + + describe('getUserSyncs', () => { + it('should return iframe syncs when iframeEnabled is true', () => { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: false + }; + const serverResponses = [{ + body: { + userSync: { + iframe: 'https://example.com/iframe' + } + } + }]; + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0]).to.deep.equal({ + type: 'iframe', + url: 'https://example.com/iframe' + }); + }); + + it('should return pixel syncs when pixelEnabled is true', () => { + const syncOptions = { + iframeEnabled: false, + pixelEnabled: true + }; + const serverResponses = [{ + body: { + userSync: { + pixel: 'https://example.com/pixel' + } + } + }]; + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0]).to.deep.equal({ + type: 'image', + url: 'https://example.com/pixel' + }); + }); + + it('should return empty array when no sync options are enabled', () => { + const syncOptions = { + iframeEnabled: false, + pixelEnabled: false + }; + const serverResponses = [{ + body: { + userSync: { + iframe: 'https://example.com/iframe', + pixel: 'https://example.com/pixel' + } + } + }]; + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + expect(syncs).to.have.lengthOf(0); + }); + }); +});