prebid/Prebid.js

View on GitHub
modules/pstudioBidAdapter.js

Summary

Maintainability
F
4 days
Test Coverage
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, VIDEO } from '../src/mediaTypes.js';
import {
  deepAccess,
  isArray,
  isNumber,
  generateUUID,
  isEmpty,
  isFn,
  isPlainObject,
} from '../src/utils.js';
import { getStorageManager } from '../src/storageManager.js';

const BIDDER_CODE = 'pstudio';
const ENDPOINT = 'https://exchange.pstudio.tadex.id/prebid-bid'
const TIME_TO_LIVE = 300;
// in case that the publisher limits number of user syncs, thisse syncs will be discarded from the end of the list
// so more improtant syncing calls should be at the start of the list
const USER_SYNCS = [
  // PARTNER_UID is a partner user id
  {
    type: 'img',
    url: 'https://match.adsrvr.org/track/cmf/generic?ttd_pid=k1on5ig&ttd_tpi=1&ttd_puid=%PARTNER_UID%&dsp=ttd',
    macro: '%PARTNER_UID%',
  },
  {
    type: 'img',
    url: 'https://dsp.myads.telkomsel.com/api/v1/pixel?uid=%USERID%',
    macro: '%USERID%',
  },
];
const COOKIE_NAME = '__tadexid';
const COOKIE_TTL_DAYS = 365;
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO];
const VIDEO_PARAMS = [
  'mimes',
  'minduration',
  'maxduration',
  'protocols',
  'startdelay',
  'placement',
  'skip',
  'skipafter',
  'minbitrate',
  'maxbitrate',
  'delivery',
  'playbackmethod',
  'api',
  'linearity',
];

export const storage = getStorageManager({ bidderCode: BIDDER_CODE });

export const spec = {
  code: BIDDER_CODE,
  supportedMediaTypes: SUPPORTED_MEDIA_TYPES,

  isBidRequestValid: function (bid) {
    const params = bid.params || {};
    return !!params.pubid && !!params.floorPrice && isVideoRequestValid(bid);
  },

  buildRequests: function (validBidRequests, bidderRequest) {
    return validBidRequests.map((bid) => ({
      method: 'POST',
      url: ENDPOINT,
      data: JSON.stringify(buildRequestData(bid, bidderRequest)),
      options: {
        contentType: 'application/json',
        withCredentials: true,
      },
    }));
  },

  interpretResponse: function (serverResponse, bidRequest) {
    const bidResponses = [];

    if (!serverResponse.body.bids) return [];
    const { id } = JSON.parse(bidRequest.data);

    serverResponse.body.bids.map((bid) => {
      const { cpm, width, height, currency, ad, meta } = bid;
      let bidResponse = {
        requestId: id,
        cpm,
        width,
        height,
        creativeId: bid.creative_id,
        currency,
        netRevenue: bid.net_revenue,
        ttl: TIME_TO_LIVE,
        meta: {
          advertiserDomains: meta.advertiser_domains,
        },
      };

      if (bid.vast_url || bid.vast_xml) {
        bidResponse.vastUrl = bid.vast_url;
        bidResponse.vastXml = bid.vast_xml;
        bidResponse.mediaType = VIDEO;
      } else {
        bidResponse.ad = ad;
      }

      bidResponses.push(bidResponse);
    });

    return bidResponses;
  },

  getUserSyncs(_optionsType, _serverResponse, _gdprConsent, _uspConsent) {
    const syncs = [];

    let userId = readUserIdFromCookie(COOKIE_NAME);

    if (!userId) {
      userId = generateId();
      writeIdToCookie(COOKIE_NAME, userId);
    }

    USER_SYNCS.map((userSync) => {
      if (userSync.type === 'img') {
        syncs.push({
          type: 'image',
          url: userSync.url.replace(userSync.macro, userId),
        });
      }
    });

    return syncs;
  },
};

function buildRequestData(bid, bidderRequest) {
  let payloadObject = buildBaseObject(bid, bidderRequest);

  if (bid.mediaTypes.banner) {
    return buildBannerObject(bid, payloadObject);
  } else if (bid.mediaTypes.video) {
    return buildVideoObject(bid, payloadObject);
  }
}

function buildBaseObject(bid, bidderRequest) {
  const firstPartyData = prepareFirstPartyData(bidderRequest.ortb2);
  const { pubid, bcat, badv, bapp } = bid.params;
  const { userId } = bid;
  const uid2Token = userId?.uid2?.id;

  if (uid2Token) {
    if (firstPartyData.user) {
      firstPartyData.user.uid2_token = uid2Token;
    } else {
      firstPartyData.user = { uid2_token: uid2Token };
    }
  }
  const userCookieId = readUserIdFromCookie(COOKIE_NAME);
  if (userCookieId) {
    if (firstPartyData.user) {
      firstPartyData.user.id = userCookieId;
    } else {
      firstPartyData.user = { id: userCookieId };
    }
  }

  return {
    id: bid.bidId,
    pubid,
    floor_price: getBidFloor(bid),
    adtagid: bid.adUnitCode,
    ...(bcat && { bcat }),
    ...(badv && { badv }),
    ...(bapp && { bapp }),
    ...firstPartyData,
  };
}

function buildBannerObject(bid, payloadObject) {
  const { sizes, pos, name } = bid.mediaTypes.banner;

  payloadObject.banner_properties = {
    name,
    sizes,
    pos,
  };

  return payloadObject;
}

function buildVideoObject(bid, payloadObject) {
  const { context, playerSize, w, h } = bid.mediaTypes.video;

  payloadObject.video_properties = {
    context,
    w: w || playerSize[0][0],
    h: h || playerSize[0][1],
  };

  for (const param of VIDEO_PARAMS) {
    const paramValue = deepAccess(bid, `mediaTypes.video.${param}`);

    if (paramValue) {
      payloadObject.video_properties[param] = paramValue;
    }
  }

  return payloadObject;
}

function readUserIdFromCookie(key) {
  try {
    const storedValue = storage.getCookie(key);

    if (storedValue !== null) {
      return storedValue;
    }
  } catch (error) {
  }
}

function generateId() {
  return generateUUID();
}

function daysToMs(days) {
  return days * DAY_IN_MS;
}

function writeIdToCookie(key, value) {
  if (storage.cookiesAreEnabled()) {
    const expires = new Date(
      Date.now() + daysToMs(parseInt(COOKIE_TTL_DAYS))
    ).toUTCString();
    storage.setCookie(key, value, expires, '/');
  }
}

function prepareFirstPartyData({ user, device, site, app, regs }) {
  let userData;
  let deviceData;
  let siteData;
  let appData;
  let regsData;

  if (user) {
    userData = {
      yob: user.yob,
      gender: user.gender,
    };
  }

  if (device) {
    deviceData = {
      ua: device.ua,
      dnt: device.dnt,
      lmt: device.lmt,
      ip: device.ip,
      ipv6: device.ipv6,
      devicetype: device.devicetype,
      make: device.make,
      model: device.model,
      os: device.os,
      osv: device.osv,
      js: device.js,
      language: device.language,
      carrier: device.carrier,
      connectiontype: device.connectiontype,
      ifa: device.ifa,
      ...(device.geo && {
        geo: {
          lat: device.geo.lat,
          lon: device.geo.lon,
          country: device.geo.country,
          region: device.geo.region,
          regionfips104: device.geo.regionfips104,
          metro: device.geo.metro,
          city: device.geo.city,
          zip: device.geo.zip,
          type: device.geo.type,
        },
      }),
      ...(device.ext && {
        ext: {
          ifatype: device.ext.ifatype,
        },
      }),
    };
  }

  if (site) {
    siteData = {
      id: site.id,
      name: site.name,
      domain: site.domain,
      page: site.page,
      cat: site.cat,
      sectioncat: site.sectioncat,
      pagecat: site.pagecat,
      ref: site.ref,
      ...(site.publisher && {
        publisher: {
          name: site.publisher.name,
          cat: site.publisher.cat,
          domain: site.publisher.domain,
        },
      }),
      ...(site.content && {
        content: {
          id: site.content.id,
          episode: site.content.episode,
          title: site.content.title,
          series: site.content.series,
          artist: site.content.artist,
          genre: site.content.genre,
          album: site.content.album,
          isrc: site.content.isrc,
          season: site.content.season,
        },
      }),
      mobile: site.mobile,
    };
  }

  if (app) {
    appData = {
      id: app.id,
      name: app.name,
      bundle: app.bundle,
      domain: app.domain,
      storeurl: app.storeurl,
      cat: app.cat,
      sectioncat: app.sectioncat,
      pagecat: app.pagecat,
      ver: app.ver,
      privacypolicy: app.privacypolicy,
      paid: app.paid,
      ...(app.publisher && {
        publisher: {
          name: app.publisher.name,
          cat: app.publisher.cat,
          domain: app.publisher.domain,
        },
      }),
      keywords: app.keywords,
      ...(app.content && {
        content: {
          id: app.content.id,
          episode: app.content.episode,
          title: app.content.title,
          series: app.content.series,
          artist: app.content.artist,
          genre: app.content.genre,
          album: app.content.album,
          isrc: app.content.isrc,
          season: app.content.season,
        },
      }),
    };
  }

  if (regs) {
    regsData = { coppa: regs.coppa };
  }

  return cleanObject({
    user: userData,
    device: deviceData,
    site: siteData,
    app: appData,
    regs: regsData,
  });
}

function cleanObject(data) {
  for (let key in data) {
    if (typeof data[key] == 'object') {
      cleanObject(data[key]);

      if (isEmpty(data[key])) delete data[key];
    }

    if (data[key] === undefined) delete data[key];
  }

  return data;
}

function isVideoRequestValid(bidRequest) {
  if (bidRequest.mediaTypes.video) {
    const { w, h, playerSize, mimes, protocols } = deepAccess(
      bidRequest,
      'mediaTypes.video',
      {}
    );

    const areSizesValid =
      (isNumber(w) && isNumber(h)) || validateSizes(playerSize);
    const areMimesValid = isArray(mimes) && mimes.length > 0;
    const areProtocolsValid =
      isArray(protocols) && protocols.length > 0 && protocols.every(isNumber);

    return areSizesValid && areMimesValid && areProtocolsValid;
  }

  return true;
}

function validateSizes(sizes) {
  return (
    isArray(sizes) &&
    sizes.length > 0 &&
    sizes.every(
      (size) => isArray(size) && size.length === 2 && size.every(isNumber)
    )
  );
}

function getBidFloor(bid) {
  if (!isFn(bid.getFloor)) {
    return bid.params.floorPrice ? bid.params.floorPrice : null;
  }

  let floor = bid.getFloor({
    currency: 'USD',
    mediaType: '*',
    size: '*',
  });
  if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') {
    return floor.floor;
  }
  return null;
}

registerBidder(spec);