prebid / Prebid.js

Setup and manage header bidding advertising partners without writing code or confusing line items. Prebid.js is open source and free.
https://docs.prebid.org
Apache License 2.0
1.33k stars 2.09k forks source link

prebidjs does not show more than 2 ad units #8438

Closed nbisso-rely closed 2 years ago

nbisso-rely commented 2 years ago

Type of issue

Bug

Description

I am using prebidjs to be able to show multiple ad units, I implemented my own adapter to call my ssp, I am returning N number of seats for the auction according to the number of units that arrives and it does not work, it always shows me the first two ad unit and then it breaks I do not know what it could be

Steps to reproduce

create new proyect

npm init -y I install these dependencies:

  "devDependencies": {
    "@babel/core": "^7.17.12",
    "@babel/preset-env": "^7.17.12",
    "babel-loader": "^8.2.5",
    "webpack": "^5.72.1",
    "webpack-cli": "^4.9.2"
  },
  "dependencies": {
    "moment": "^2.29.3",
    "prebid.js": "^6.24.1"
  }

webpack.config:

const path = require('path');

module.exports = {
    mode: "development",
    entry: path.resolve(__dirname, './src/index.js'),
    module: {
        rules: [
            {
                test: /\.(js)$/,
                exclude: /node_modules/,
                use: ['babel-loader']
            },
            // this separate rule is required to make sure that the Prebid.js files are babel-ified.  this rule will
            // override the regular exclusion from above (for being inside node_modules).
            {
                test: /.js$/,
                include: new RegExp(`\\${path.sep}prebid\\.js`),
                use: {
                    loader: 'babel-loader',
                    // presets and plugins for Prebid.js must be manually specified separate from your other babel rule.
                    // this can be accomplished by requiring prebid's .babelrc.js file (requires Babel 7 and Node v8.9.0+)
                    // as of Prebid 6, babelrc.js only targets modern browsers. One can change the targets and build for
                    // older browsers if they prefer, but integration tests on ie11 were removed in Prebid.js 6.0
                    options: require('prebid.js/.babelrc.js')
                }
            },
        ],

    },
    resolve: {
        extensions: ['*', '.js']
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'sspmedia.js',
        clean: true,

    },
    devServer: {
        static: path.resolve(__dirname, './dist'),
        filename: 'sspmedia.js',
        clean: true,
    },
};

and I create my adapter and my code to obtain the ad units

index.js:

import pbjs from 'prebid.js';
import { registerBidder } from 'prebid.js/src/adapters/bidderFactory.js';
import { spec } from './adpater';
import { on } from "prebid.js/src/events.js"

registerBidder(spec)

pbjs.processQueue();

function initRelySsp() {

    const pbjs_TIMEOUT = 1000;
    const FAILSAFE_TIMEOUT = 5000;
    const relyBidder = {
        bids: [{
            bidder: 'idx',
            params: {
                ssp_id: 'test',
                ad_space_id: 'test'
            }
        }]
    }
    const adUnits = []
    const slots = []
    document.querySelectorAll('[data-rely-slot]').forEach(item => {
        let slot = JSON.parse(item.getAttribute('data-rely-slot').replaceAll("'", "\""))
        slots.push(slot)
        adUnits.push({
            code: slot.code,
            mediaTypes: { ...slot.mediaTypes },
            bids: [...relyBidder.bids]
        })
    })

    console.log(adUnits)

    window.googletag = window.googletag || {};
    window.googletag.cmd = window.googletag.cmd || [];
    window.googletag.cmd.push(function () {
        googletag.pubads().disableInitialLoad();
    });

    pbjs.que = pbjs.que || [];

    pbjs.que.push(function () {
        pbjs.addAdUnits(adUnits);
        pbjs.requestBids({
            bidsBackHandler: initAdserver,
            timeout: pbjs_TIMEOUT
        });
    });

    function initAdserver() {
        if (pbjs.initAdserverSet) return;
        pbjs.initAdserverSet = true;
        window.googletag.cmd.push(function () {
            pbjs.setTargetingForGPTAsync && pbjs.setTargetingForGPTAsync();
            window.googletag.pubads().refresh();
        });
    }

    // in case pbjs doesn't load
    setTimeout(function () {
        initAdserver();
    }, FAILSAFE_TIMEOUT);

    slots.forEach(slot => {
        window.googletag.cmd.push(function () {

            window.googletag.defineSlot(slot.code, slot.sizes, slot.code)
                .addService(googletag.pubads());

            window.googletag.pubads().enableSingleRequest();
            window.googletag.enableServices();
        });

    })

    slots.forEach(slot => {
        window.googletag.cmd.push(function () {
            window.googletag.display(slot.code);
        });
    })
}

console.log("RELY SSP INIT with pbjs: ", pbjs)

initRelySsp()

adapter.js:

import * as utils from 'prebid.js/src/utils';
// import { BANNER, NATIVE, VIDEO } from 'prebid.js/src/mediaTypes';

const TTL = 500;
const BIDDER_CODE = 'idx';
const SUPPORTED_USER_IDS = ['idx'];
const DEFAULT_CURRENCY = 'USD';
const BASE_BID_FLOOR = 0.1;
const USER_ID_MATCHER = {
    'idx': 'idx.lat',
};

const vars = {
    dev: {
        url: 'http://localhost:8080/test'
    },
    prod: {
        url: 'http://localhost:8080/test'
    }
};

const OPENRTB = {
    NATIVE: {
        IMAGE_TYPE: {
            ICON: 1,
            MAIN: 3,
        },
        ASSET_ID: {
            TITLE: 1,
            IMAGE: 2,
            ICON: 3,
            BODY: 4,
            SPONSORED: 5,
            CTA: 6
        },
        DATA_ASSET_TYPE: {
            SPONSORED: 1,
            DESC: 2,
            CTA_TEXT: 12,
        },
    }
};

export const spec = {
    code: BIDDER_CODE,
    supportedMediaTypes: ["banner", "video", "native"],

    /**
     * Determines whether or not the given bid request is valid.
     *
     * @param {BidRequest} bid The bid params to validate.
     * @return boolean True if this is a valid bid, and false otherwise.
     */
    isBidRequestValid: function (bid) {
        console.log("isBidRequestValid", bid.params.ssp_id && bid.params.ad_space_id)
        return bid.params.ssp_id && bid.params.ad_space_id;
    },

    /**
     * Make a server request from the list of BidRequests.
     *
     * @param {validBidRequests[]} validBidRequests an array of bids
     * @param {BidderRequest} bidderRequest
     * @return ServerRequest Info describing the request to the server.
     */
    buildRequests: function (validBidRequests, bidderRequest) {
        console.log(validBidRequests)
        const bid = validBidRequests[0];
        const env = bid.params.env || 'prod';
        const request = {
            id: bid.auctionId,
            test: bid.params.test || 0,
            imp: validBidRequests.map(slot => mapImpression(slot, bid.params)),
            site: getSite(bid),
            app: getApp(bid),
            device: getDevice(bid),
            source: mapSource(bid),
            ext: {
                ssp_id: bid.params.ssp_id.toString(),
                ad_space_id: bid.params.ad_space_id.toString(),
            }
        };

        setSupportedUserIds(request, bid);

        if (!request.app) {
            delete request.app;
        }

        if (!request.site) {
            delete request.site;
        }

        const fullReq = {
            method: 'POST',
            url: vars[env].url,
            options: {
                contentType: 'application/json',
                withCredentials: false,
                customHeaders: {
                    'X-Openrtb-Version': '2.5',
                }
            },
            data: JSON.stringify(request)
        };

        console.log(bidderRequest);

        return fullReq;
    },

    /**
     * Unpack the response from the server into a list of bids.
     *
     * @param {ServerResponse} serverResponse A successful response from the server.
     * @param {BidRequest} bidRequest The initial bid request.
     * @return {Bid[]} An array of bids which were nested inside the server.
     */
    interpretResponse: function (serverResponse, bidRequest) {
        let bids = [];

        console.log('Server response:');
        console.log(serverResponse);

        for (let i = 0; i < serverResponse.body.seatbid.length; i++) {
            const seatbid = serverResponse.body.seatbid[i];
            const serverBody = seatbid.bid;
            try {
                if (!utils.isArray(serverBody)) {
                    return [];
                }

                for (const serverBid of serverBody) {
                    if (serverBid.price === 0) {
                        continue;
                    }

                    if (serverBid.adm.indexOf('{') === 0) {
                        bids.push(getPrebidNativeBid(serverBid, serverResponse.body.cur));
                    } else {
                        bids.push(getPrebidBannerBid(serverBid, serverResponse.body.cur));
                    }
                }
            } catch (e) {
                console.log(e);
            }
        }

        console.log("returned bids ===>", bids)
        return bids;
    },

    /**
     * Register the user sync pixels which should be dropped after the auction.
     *
     * @param {SyncOptions} syncOptions Which user syncs are allowed?
     * @param {ServerResponse[]} serverResponses List of server's responses.
     * @return {UserSync[]} The user syncs which should be dropped.
     */
    getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) {
        const syncs = [];

        /* var gdpr_params;
        if (typeof gdprConsent.gdprApplies === 'boolean') {
          gdpr_params = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`;
        } else {
          gdpr_params = `gdpr_consent=${gdprConsent.consentString}`;
        }

        if (syncOptions.iframeEnabled) {
          syncs.push({
            type: 'iframe',
            url: '//acdn.adnxs.com/ib/static/usersync/v3/async_usersync.html?' + gdpr_params
          });
        }
        if (syncOptions.pixelEnabled && serverResponses.length > 0) {
          syncs.push({
            type: 'image',
            url: serverResponses[0].body.userSync.url + gdpr_params
          });
        } */
        return syncs;
    }
    ,
};

/**
 * @param {object} slot Ad Unit Params by Prebid
 * @returns {object} Source by OpenRTB 2.5 §3.2.2
 */
function mapSource(slot) {
    const source = {
        tid: slot.transactionId,
    };

    const schain = mapSchain(slot.schain);

    if (schain) {
        source.ext = {
            schain: schain
        }
    }

    return source;
}

/**
 * @param {object} schain object set by Publisher
 * @returns {object} OpenRTB SupplyChain object
 */
function mapSchain(schain) {
    if (!schain) {
        return null;
    }
    if (!validateSchain(schain)) {
        utils.logError('RTB House: required schain params missing');
        return null;
    }
    return schain;
}

/**
 * @param {object} schain object set by Publisher
 * @returns {object} bool
 */
function validateSchain(schain) {
    if (!schain.nodes) {
        return false;
    }
    const requiredFields = ['asi', 'sid', 'hp'];
    return schain.nodes.every(node => {
        return requiredFields.every(field => node[field]);
    });
}

/**
 * @param {object} slot Ad Unit Slot by Prebid
 * @param {object} params Ad Unit Params by Prebid
 * @returns {object} Imp by OpenRTB 2.5 §3.2.4
 */
function mapImpression(slot, params) {
    console.log("mapImpression", slot, params)
    const imp = {
        id: slot.bidId,
        banner: mapBanner(slot),
        native: mapNative(slot),
        displaymanager: params.displaymanager,
        displaymanagerver: params.displaymanagerver,
        instl: params.instl,
    };

    setBidFloor(slot, imp, params);

    return imp;
}

/**
 * @param {object} slot Ad Unit Params by Prebid
 * @returns {object} Request by OpenRTB Native Ads 1.1 §4
 */
function mapNative(slot) {
    if (slot.mediaType === 'native' || utils.deepAccess(slot, 'mediaTypes.native')) {
        return {
            request: {
                assets: mapNativeAssets(slot)
            },
            ver: '1.1'
        }
    }
}

/**
 * @param {object} slot Slot config by Prebid
 * @returns {array} Request Assets by OpenRTB Native Ads 1.1 §4.2
 */
function mapNativeAssets(slot) {
    const params = slot.nativeParams || utils.deepAccess(slot, 'mediaTypes.native');
    const assets = [];
    if (params.title) {
        assets.push({
            id: OPENRTB.NATIVE.ASSET_ID.TITLE,
            required: params.title.required ? 1 : 0,
            title: {
                len: params.title.len || 25
            }
        })
    }
    if (params.image) {
        assets.push({
            id: OPENRTB.NATIVE.ASSET_ID.IMAGE,
            required: params.image.required ? 1 : 0,
            img: mapNativeImage(params.image, OPENRTB.NATIVE.IMAGE_TYPE.MAIN)
        })
    }
    if (params.icon) {
        assets.push({
            id: OPENRTB.NATIVE.ASSET_ID.ICON,
            required: params.icon.required ? 1 : 0,
            img: mapNativeImage(params.icon, OPENRTB.NATIVE.IMAGE_TYPE.ICON)
        })
    }
    if (params.sponsoredBy) {
        assets.push({
            id: OPENRTB.NATIVE.ASSET_ID.SPONSORED,
            required: params.sponsoredBy.required ? 1 : 0,
            data: {
                type: OPENRTB.NATIVE.DATA_ASSET_TYPE.SPONSORED,
                len: params.sponsoredBy.len
            }
        })
    }
    if (params.body) {
        assets.push({
            id: OPENRTB.NATIVE.ASSET_ID.BODY,
            required: params.body.request ? 1 : 0,
            data: {
                type: OPENRTB.NATIVE.DATA_ASSET_TYPE.DESC,
                len: params.body.len
            }
        })
    }
    if (params.cta) {
        assets.push({
            id: OPENRTB.NATIVE.ASSET_ID.CTA,
            required: params.cta.required ? 1 : 0,
            data: {
                type: OPENRTB.NATIVE.DATA_ASSET_TYPE.CTA_TEXT,
                len: params.cta.len
            }
        })
    }
    return assets;
}

/**
 * @param {object} image Prebid native.image/icon
 * @param {int} type Image or icon code
 * @returns {object} Request Image by OpenRTB Native Ads 1.1 §4.4
 */
function mapNativeImage(image, type) {
    const img = { type: type };
    if (image.aspect_ratios) {
        const ratio = image.aspect_ratios[0];
        const minWidth = ratio.min_width || 100;
        img.wmin = minWidth;
        img.hmin = (minWidth / ratio.ratio_width * ratio.ratio_height);
    }
    if (image.sizes) {
        const size = Array.isArray(image.sizes[0]) ? image.sizes[0] : image.sizes;
        img.w = size[0];
        img.h = size[1];
    }
    return img
}

/**
 * @param {object} slot Ad Unit Params by Prebid
 * @returns {object} Banner by OpenRTB 2.5 §3.2.6
 */
function mapBanner(slot) {
    if (slot.mediaType === 'banner' ||
        utils.deepAccess(slot, 'mediaTypes.banner') ||
        (!slot.mediaType && !slot.mediaTypes)) {
        let sizes = slot.sizes || slot.mediaTypes.banner.sizes;

        return {
            w: sizes[0][0],
            h: sizes[0][1],
            format: sizes.map(size => ({
                w: size[0],
                h: size[1]
            }))
        };
    }
}

/**
 * @param {object} serverBid Bid by OpenRTB 2.5 §4.2.3
 * @returns {object} Prebid banner bidObject
 */
function getPrebidBannerBid(serverBid, bidCurrency) {
    return {
        requestId: serverBid.impid,
        mediaType: "banner",
        cpm: serverBid.price,
        creativeId: serverBid.crid,
        ad: serverBid.adm,
        width: serverBid.w,
        height: serverBid.h,
        ttl: TTL,
        netRevenue: true,
        currency: bidCurrency || DEFAULT_CURRENCY,
    }
}

/**
 * @param {object} serverBid Bid by OpenRTB 2.5 §4.2.3
 * @returns {object} Prebid native bidObject
 */
function getPrebidNativeBid(serverBid, bidCurrency) {
    return {
        requestId: serverBid.impid,
        mediaType: "native",
        cpm: serverBid.price,
        creativeId: serverBid.adid,
        width: 1,
        height: 1,
        ttl: TTL,
        netRevenue: true,
        currency: bidCurrency || DEFAULT_CURRENCY,
        native: getPrebidNativeAd(serverBid.adm),
    }
}

/**
 * @param {string} adm JSON-encoded Request by OpenRTB Native Ads 1.1 §4.1
 * @returns {object} Prebid bidObject.native
 */
function getPrebidNativeAd(adm) {
    const native = JSON.parse(adm).native;

    const result = {
        clickUrl: encodeURIComponent(native.link.url),
        impressionTrackers: native.imptrackers
    };

    for (const asset of native.assets) {
        switch (asset.id) {
            case OPENRTB.NATIVE.ASSET_ID.TITLE:
                result.title = asset.title.text;
                break;
            case OPENRTB.NATIVE.ASSET_ID.IMAGE:
                result.image = {
                    url: encodeURIComponent(asset.img.url),
                    width: asset.img.w,
                    height: asset.img.h
                };
                break;
            case OPENRTB.NATIVE.ASSET_ID.ICON:
                result.icon = {
                    url: encodeURIComponent(asset.img.url),
                    width: asset.img.w,
                    height: asset.img.h
                };
                break;
            case OPENRTB.NATIVE.ASSET_ID.BODY:
                result.body = asset.data.value;
                break;
            case OPENRTB.NATIVE.ASSET_ID.SPONSORED:
                result.sponsoredBy = asset.data.value;
                break;
            case OPENRTB.NATIVE.ASSET_ID.CTA:
                result.cta = asset.data.value;
                break;
        }
    }

    return result;
}

function setSupportedUserIds(requestData, bidRequest) {
    if (typeof requestData.user == 'undefined') {
        utils.deepSetValue(requestData, 'user', {});
    }

    if (bidRequest.userId) {
        for (const userId in bidRequest.userId) {
            if (bidRequest.userId.hasOwnProperty(userId) && SUPPORTED_USER_IDS.includes(userId)) {
                if (typeof requestData.user.ext == 'undefined') {
                    utils.deepSetValue(requestData, 'user.ext.eids', []);
                }

                let userIdAsEid = bidRequest.userIdAsEids.find(u => USER_ID_MATCHER[userId] === u.source);

                requestData.user.ext.eids.push({
                    source: userIdAsEid.source,
                    uids: [{
                        id: bidRequest.userId[userId],
                        ext: {
                            rtiPartner: userId
                        }
                    }]
                });
            }
        }
    }
}

function setBidFloor(bidRequest, imp, params) {
    let bidFloor = -1;

    // Using bidfloor service
    if (typeof bidRequest.getFloor === 'function') {
        ["banner", "video", "native"].forEach(mediaType => {
            if (imp.hasOwnProperty(mediaType)) {
                let floorInfo = bid.getFloor({ currency: imp.bidfloorcur, mediaType: mediaType, size: '*' });
                if (typeof floorInfo === 'object' && floorInfo.currency === imp.bidfloorcur && !isNaN(parseInt(floorInfo.floor))) {
                    let mediaTypeFloor = parseFloat(floorInfo.floor);
                    bidFloor = (bidFloor === -1 ? mediaTypeFloor : Math.min(mediaTypeFloor, bidFloor))
                }
            }
        });
    }

    if (imp.bidfloor) {
        bidFloor = Math.max(bidFloor, imp.bidfloor)
    }

    // set flor on impression
    imp.bidfloor = ((!isNaN(bidFloor) && bidFloor > 0) ? bidFloor : (params.bidfloor || BASE_BID_FLOOR));
    imp.bidfloorcur = params.bidfloorcur || DEFAULT_CURRENCY;
}

function getSite(bidRequest) {
    const siteParams = utils.deepAccess(bidRequest, 'params.site');

    if (siteParams) {
        return {
            publisher: {
                id: siteParams.pubId.toString(),
                domain: window.location.hostname,
            },
            id: siteParams.id.toString(),
            ref: window.top.document.referrer,
            page: window.location.href,
        }
    }

    return null;
}

function getApp(bidRequest) {
    const appParams = utils.deepAccess(bidRequest, 'params.app');

    if (appParams) {
        return {
            id: appParams.id,
            name: appParams.name,
            bundle: appParams.bundle,
            storeurl: appParams.storeUrl,
            domain: appParams.domain,
        }
    }

    return null;
}

function getDevice(bidRequest) {
    let device = {
        dnt: utils.getDNT() ? 1 : 0,
        os: navigator.platform,
        ua: navigator.userAgent,
        language: (navigator.language || navigator.browserLanguage),
        w: (window.screen.width || window.innerWidth),
        h: (window.screen.height || window.innerHeigh),
        ip: '',
    };

    const geo = utils.deepAccess(bidRequest, 'params.geo');
    if (geo) {
        utils.deepSetValue(device, 'geo', geo);
    }

    const ifa = utils.deepAccess(bidRequest, 'params.ifa');
    if (ifa) {
        utils.deepSetValue(device, 'ifa', ifa);
    }

    const ip = utils.deepAccess(bidRequest, 'params.ip');
    if (ip) {
        utils.deepSetValue(device, 'ip', ip);
    }

    return device;
}

the ssp api returns:

{"id":"5c4a5f23-dcff-4fb5-9220-c2879b90a225","seatbid":[{"bid":[{"id":"8a2525f9-87e0-5f56-df7e-f717f1c5dee3","impid":"254d675ce127a","price":450,"adm":"\u003ca target=\"_blank\" href=\"http://google.com\"\u003e\u003cimg src=\"https://www.gourmetads.com/wp-content/uploads/2019/02/300x250-barilla.jpg?id=1\"/\u003e\u003c/a\u003e","adomain":["test.com"],"crid":"1","dealid":"1","w":300,"h":250}]},{"bid":[{"id":"8d1f5fee-f71a-d73f-3f8a-d96ab06e46f7","impid":"32f392f51915f8","price":450,"adm":"\u003ca target=\"_blank\" href=\"http://google.com\"\u003e\u003cimg src=\"https://www.gourmetads.com/wp-content/uploads/2019/02/300x250-barilla.jpg?id=2\"/\u003e\u003c/a\u003e","adomain":["test.com"],"crid":"1","dealid":"2","w":728,"h":90}]},{"bid":[{"id":"fcccdfd5-2f80-2d09-3ec2-938025b2c604","impid":"420ad08b684dfa","price":450,"adm":"\u003ca target=\"_blank\" href=\"http://google.com\"\u003e\u003cimg src=\"https://www.gourmetads.com/wp-content/uploads/2019/02/300x250-barilla.jpg?id=3\"/\u003e\u003c/a\u003e","adomain":["test.com"],"crid":"1","dealid":"3","w":728,"h":90}]}],"bidid":"a2a6d307-008c-8da8-bb34-b6745c87fc7d","cur":"USD"}

and my test.html code is:

<html>

<head>
    <link rel="icon" type="image/png" href="/favicon.png">
    <script async src="//www.googletagservices.com/tag/js/gpt.js"></script>
</head>

<body>
    <h2>Basic Prebid.js Example</h2>
    <h5>Div-1</h5>
    <div id='/19968336/header-bid-tag-1'
        data-rely-slot="{'sizes': [[300,250]],'code': '/19968336/header-bid-tag-1','mediaTypes': {'banner': {'sizes': [[300,250]]}}}">
    </div>
    <div id='/19968336/header-bid-tag-2'
        data-rely-slot="{'sizes': [[300,250]],'code': '/19968336/header-bid-tag-2','mediaTypes': {'banner': {'sizes': [[300,250]]}}}">
    </div>
    <div id='/19968336/header-bid-tag-3'
        data-rely-slot="{'sizes': [[300,250]],'code': '/19968336/header-bid-tag-3','mediaTypes': {'banner': {'sizes': [[300,250]]}}}">
    </div>
    <div id='/19968336/header-bid-tag-4'
        data-rely-slot="{'sizes': [[300,250]],'code': '/19968336/header-bid-tag-4','mediaTypes': {'banner': {'sizes': [[300,250]]}}}">
    </div>

    <script async src="./dist/sspmedia.js"></script>

</body>

</html>

Expected results

render OK all ad units

Actual results

only render 2 ads: result in console i dont have any error

Platform details

prebid.js version: 6.24.1 node version: v16.14.0

Other information

mkendall07 commented 2 years ago

I'd try using your own prebid line within your GAM account. You might be hitting a limit on the number of creative being returned from the same GAM line.

nbisso-rely commented 2 years ago

@mkendall07 hello! thanks for your quick response. Do I need to have a GAM account? I can't call my ssp directly without having to configure anything from the GAM?

nbisso-rely commented 2 years ago

solved by ignoring google ad server script: https://github.com/prebid/Prebid.js/pull/7308