prebid/Prebid.js

View on GitHub
modules/smilewantedBidAdapter.js

Summary

Maintainability
D
1 day
Test Coverage
import {deepAccess, deepClone, isArray, isFn, isPlainObject, logError, logWarn} from '../src/utils.js';
import {Renderer} from '../src/Renderer.js';
import {config} from '../src/config.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js';
import {INSTREAM, OUTSTREAM} from '../src/video.js';
import {convertOrtbRequestToProprietaryNative, toOrtbNativeRequest, toLegacyResponse} from '../src/native.js';

const BIDDER_CODE = 'smilewanted';

/**
 * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
 * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
 * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest
 * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse
 * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions
 * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync
 */

const GVL_ID = 639;

export const spec = {
  code: BIDDER_CODE,
  gvlid: GVL_ID,
  aliases: ['smile', 'sw'],
  supportedMediaTypes: [BANNER, VIDEO, NATIVE],
  /**
   * Determines whether or not the given bid request is valid.
   *
   * @param {BidRequest} bid The bid to validate.
   * @return boolean True if this is a valid bid, and false otherwise.
   */
  isBidRequestValid: function(bid) {
    if (!bid.params || !bid.params.zoneId) {
      return false;
    }

    if (deepAccess(bid, 'mediaTypes.video')) {
      const videoMediaTypesParams = deepAccess(bid, 'mediaTypes.video', {});
      const videoBidderParams = deepAccess(bid, 'params.video', {});

      const videoParams = {
        ...videoMediaTypesParams,
        ...videoBidderParams
      };

      if (!videoParams.context || ![INSTREAM, OUTSTREAM].includes(videoParams.context)) {
        return false;
      }
    }

    return true;
  },

  /**
   * Make a server request from the list of BidRequests.
   *
   * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server.
   * @param {BidderRequest} bidderRequest bidder request object.
   * @return ServerRequest Info describing the request to the server.
   */
  buildRequests: function(validBidRequests, bidderRequest) {
    validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests);

    return validBidRequests.map(bid => {
      const payload = {
        zoneId: bid.params.zoneId,
        currencyCode: config.getConfig('currency.adServerCurrency') || 'EUR',
        tagId: bid.adUnitCode,
        sizes: bid.sizes.map(size => ({
          w: size[0],
          h: size[1]
        })),
        transactionId: bid.ortb2Imp?.ext?.tid,
        timeout: bidderRequest?.timeout,
        bidId: bid.bidId,
        /**
         positionType is undocumented
        It is unclear what this parameter means.
        If it means the same as pos in openRTB,
        It should read from openRTB object
        or from mediaTypes.banner.pos
         */
        positionType: bid.params.positionType || '',
        prebidVersion: '$prebid.version$'
      };

      const floor = getBidFloor(bid);
      if (floor) {
        payload.bidfloor = floor;
      }

      if (bid.params.bidfloor) {
        payload.bidfloor = bid.params.bidfloor;
      }

      if (bidderRequest?.refererInfo) {
        payload.pageDomain = bidderRequest.refererInfo.page || '';
      }

      if (bidderRequest?.gdprConsent) {
        payload.gdpr_consent = bidderRequest.gdprConsent.consentString;
        payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side
      }

      payload.eids = bid?.userIdAsEids;

      const videoMediaType = deepAccess(bid, 'mediaTypes.video');
      const context = deepAccess(bid, 'mediaTypes.video.context');

      if (bid.mediaType === 'video' || (videoMediaType && context === INSTREAM) || (videoMediaType && context === OUTSTREAM)) {
        payload.context = context;
        payload.videoParams = deepClone(videoMediaType);
      }

      const nativeMediaType = deepAccess(bid, 'mediaTypes.native');

      if (nativeMediaType) {
        payload.context = 'native';
        payload.nativeParams = nativeMediaType;
        let sizes = deepAccess(bid, 'mediaTypes.native.image.sizes', []);

        if (sizes.length > 0) {
          const size = Array.isArray(sizes[0]) ? sizes[0] : sizes;

          payload.width = size[0] || payload.width;
          payload.height = size[1] || payload.height;
        }
      }

      const payloadString = JSON.stringify(payload);
      return {
        method: 'POST',
        url: 'https://prebid.smilewanted.com',
        data: payloadString,
      };
    });
  },

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

    try {
      const response = serverResponse.body;
      const bidRequestData = JSON.parse(bidRequest.data);
      if (response) {
        const dealId = response.dealId || '';
        const bidResponse = {
          requestId: bidRequestData.bidId,
          cpm: response.cpm,
          width: response.width,
          height: response.height,
          creativeId: response.creativeId,
          dealId: response.dealId,
          currency: response.currency,
          netRevenue: response.isNetCpm,
          ttl: response.ttl,
          ad: response.ad,
        };

        if (response.formatTypeSw === 'video_instream' || response.formatTypeSw === 'video_outstream') {
          bidResponse['mediaType'] = 'video';
          bidResponse['vastUrl'] = response.ad;
          bidResponse['ad'] = null;

          if (response.formatTypeSw === 'video_outstream') {
            bidResponse['renderer'] = newRenderer(bidRequestData, response);
          }
        }

        if (response.formatTypeSw === 'native') {
          const nativeAdResponse = JSON.parse(response.ad);
          const ortbNativeRequest = toOrtbNativeRequest(bidRequestData.nativeParams);
          bidResponse['mediaType'] = 'native';
          bidResponse['native'] = toLegacyResponse(nativeAdResponse, ortbNativeRequest);
        }

        if (dealId.length > 0) {
          bidResponse.dealId = dealId;
        }

        bidResponse.meta = {};
        if (response.meta?.advertiserDomains && isArray(response.meta.advertiserDomains)) {
          bidResponse.meta.advertiserDomains = response.meta.advertiserDomains;
        }
        bidResponses.push(bidResponse);
      }
    } catch (error) {
      logError('Error while parsing smilewanted response', error);
    }

    return bidResponses;
  },

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

    if (gdprConsent && typeof gdprConsent.consentString === 'string') {
      // add 'gdpr' only if 'gdprApplies' is defined
      if (typeof gdprConsent.gdprApplies === 'boolean') {
        params += `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`;
      } else {
        params += `?gdpr_consent=${gdprConsent.consentString}`;
      }
    }

    if (uspConsent) {
      params += `${params ? '&' : '?'}us_privacy=${encodeURIComponent(uspConsent)}`;
    }

    const syncs = []

    if (syncOptions.iframeEnabled) {
      syncs.push({
        type: 'iframe',
        url: 'https://csync.smilewanted.com' + params
      });
    }

    return syncs;
  }
}

/**
 * Create SmileWanted renderer
 * @param bidRequest
 * @param bidResponse
 * @returns {*}
 */
function newRenderer(bidRequest, bidResponse) {
  const renderer = Renderer.install({
    id: bidRequest.bidId,
    url: bidResponse.OustreamTemplateUrl,
    loaded: false
  });

  try {
    renderer.setRender(outstreamRender);
  } catch (err) {
    logWarn('Prebid Error calling setRender on newRenderer', err);
  }
  return renderer;
}

/**
 * Initialise SmileWanted outstream
 * @param bid
 */
function outstreamRender(bid) {
  bid.renderer.push(() => {
    window.SmileWantedOutStreamInit({
      width: bid.width,
      height: bid.height,
      vastUrl: bid.vastUrl,
      elId: bid.adUnitCode
    });
  });
}

/**
 * Get the floor price from bid.params for backward compatibility.
 * If not found, then check floor module.
 * @param bid A valid bid object
 * @returns {*|number} floor price
 */
function getBidFloor(bid) {
  if (isFn(bid.getFloor)) {
    const floorInfo = bid.getFloor({
      currency: 'USD',
      mediaType: 'banner',
      size: bid.sizes.map(size => ({ w: size[0], h: size[1] }))
    });
    if (isPlainObject(floorInfo) && !isNaN(floorInfo.floor) && floorInfo.currency === 'USD') {
      return parseFloat(floorInfo.floor);
    }
  }
  return null;
}

registerBidder(spec);