Headerbidding Injection custom Drupal module

Developed a custom module for injecting Prebid.JS and Amazon header bidding components to improve ad-based earnings from the Google Ad marketplace.

Completion Date
Prebid.JS thumbnail
Platform(s)/Language(s)
Code Snippet
/**
 * @file
 * Sets up necessary config settings & scripts for Header Bidding.
 */
 
/**
 * Implements hook_preprocess_HOOK().
 */
function mm_prebid_preprocess_html(&$vars) {
  if (!in_array('page-admin', $vars['classes_array'])) {
    // Only call for prebidding on public-facing pages.
    MmPrebid::preprocessHeaderBidding($vars['page']);
  }
}
/**
 * @file
 * Classes for MM Prebid.
 */
 
/**
 * MM Prebid class.
 */
class MmPrebid {
 
  /**
   * Add in Prebid.js config code to all page headers.
   *
   * @param array $page
   *   Associated array of page regions and content.
   */
  public static function preprocessHeaderBidding(array $page) {
    /* Set `$pbjs_filename` to the compiled pbjs file downloaded from
     * http://prebid.org/download.html and saved to `js/prebid1.xx.y.js`.
     */
    $pbjs_filename = 'prebid1.36.0.js';
 
    $headerbid_elems = array();
 
    // Get headerbid params to parse for Prebid.JS and Amazon.
    $headerbid_units = mm_api_get_siteconfig_value('headerbidding');
 
    if (isset($headerbid_units['prebidjs'])) {
      // Loop thru the ad slots for PBJS & match them up with dfp id divs.
      foreach ($headerbid_units['prebidjs'] as $slot => $networks) {
        if ($slot == 'OoyalaPreRoll') {
          // If the slot is for Ooyala, then assign it a hardcoded id value.
          $dfp_id = 'jwplayer-container-1';
        }
        else {
          $dfp_id = mm_api_get_siteconfig_value('dfp', $slot);
        }
 
        if (!empty($dfp_id) && MmPrebid::arrayValueSearch($page, $dfp_id, 0)) {
          // If the dfp id is present on the current page, then add it to
          // $headerbid_elems.
          $headerbid_elems['prebidjs'][] = array(
            'dfpId' => $dfp_id,
            'placementIds' => $networks,
          );
        }
      }
      // If any PBJS ids were found on the current page, then load the Prebid.JS
      // files.
      if (isset($headerbid_elems['prebidjs'])) {
        // First, add additional config settings for PBJS to $headerbid_elems.
        $headerbid_elems['extra_config']['prebidjs'] =
          $headerbid_units['extra_config']['prebidjs'] ?? [];
        drupal_add_js(drupal_get_path('module', 'mm_prebid') . '/js/' . $pbjs_filename, [
          'weight' => -6,
        ]);
        drupal_add_js(drupal_get_path('module', 'mm_prebid') . '/js/prebid-config.js', [
          'weight' => -5,
        ]);
      }
    }
 
    if (isset($headerbid_units['amazon'])) {
      // Loop thru the ad slots for APS & match them up with dfp id divs.
      foreach ($headerbid_units['amazon'] as $slot) {
        if ($dfp_id = mm_api_get_siteconfig_value('dfp', $slot)) {
          // If the dfp id is present on the current page, then add it to
          // $headerbid_elems.
          $is_dfp_id_present = MmPrebid::arrayValueSearch($page, $dfp_id, 0);
          if ($is_dfp_id_present) {
            $headerbid_elems['amazon'][] = array(
              'dfpId' => $dfp_id,
            );
          }
        }
      }
      // If any Amazon ids were found on the current page, then load the APS JS
      // files.
      if (isset($headerbid_elems['amazon'])) {
        // First, add additional config settings for PBJS to $headerbid_elems.
        $headerbid_elems['extra_config']['amazon'] =
          $headerbid_units['extra_config']['amazon'] ?? [];
        drupal_add_js(drupal_get_path('module', 'mm_prebid') . '/js/apstag.js', [
          'weight' => -4,
        ]);
        drupal_add_js(drupal_get_path('module', 'mm_prebid') . '/js/apstag-config.js', [
          'weight' => -3,
        ]);
      }
    }
 
    // If any headerbid ids of any kind were found on the current page, the load
    // the generic headerbid config JS file.
    if (count($headerbid_elems)) {
      drupal_add_js(['mm_prebid' => ['headerbidElems' => $headerbid_elems]], [
        'type' => 'setting',
        'weight' => -8,
      ]);
      drupal_add_js(drupal_get_path('module', 'mm_prebid') . '/js/headerbid-config.js', [
        'weight' => -7,
      ]);
    }
  }
 
  /**
   * Recursively search multidimensional array values for occurrence of a string.
   *
   * @param mixed $data
   *   Multidimensional associated array to search.
   * @param string $search_string
   *   Value for which to search.
   * @param int $max_nest_level
   *   Maximum number of nested levels to recursively search an array.
   * @param int $nest_level
   *   Current nesting level being searched.
   * @param string $result_key
   *   Recursively-built array key index pointing to search result.
   *
   * @return string
   *   Array key index if string is found. '' otherwise.
   */
  public static function arrayValueSearch(
    $data,
    string $search_string,
    int $max_nest_level = 3,
    int $nest_level = 0,
    string $result_key = ''
  ) {
    if ($nest_level > $max_nest_level) {
      return '';
    }
    else {
      foreach ($data as $key => $value) {
        if (is_array($value)) {
          $result_key .= MmPrebid::arrayValueSearch($value, $search_string,
            $max_nest_level, $nest_level + 1);
        }
        elseif (is_object($value)) {
          $result_key .= MmPrebid::arrayValueSearch($value, $search_string,
            $max_nest_level, $nest_level + 1);
        }
        elseif (strpos((string) $value, $search_string) !== FALSE) {
          $result_key .= '[' . $key . ']';
          return $result_key;
        }
 
        if ($result_key !== '') {
          return '[' . $key . ']' . $result_key;
        }
      }
 
      return $result_key;
    }
  }
 
}
/**
 * @file
 * Prebid.JS config code.
 *
 * Reference: http://jsfiddle.net/prebid/e48yrvby/1/
 */
 
// Declare some global variables.
var PREBID_TIMEOUT = 700;
var pbjs = pbjs || {};
var pbjsUnits = pbjsUnits || [];
var pbjsVideoUnits = pbjsVideoUnits || [];
var googletag = googletag || {};
 
// Initialize Prebid.js array and prepare it for accepting bids.
pbjs.que = pbjs.que || [];
 
function sendAdserverRequest() {
  /* console.log("Invoked sendAdserverRequest(), in prebid-config.js."); */
  if (pbjs.adserverRequestSent) {
    /* console.log("  pbjs.adserverRequestSent is already true. Calling refreshPubAds()."); */
    refreshPubAds();
    return;
  }
  else {
    /* console.log("  Sending prebid.js ad server request."); */
    pbjs.adserverRequestSent = true;
    googletag.cmd.push(function () {
      pbjs.que.push(function () {
        pbjs.setTargetingForGPTAsync();
        /* console.log("    Calling refreshPubAds() via googletag.cmd, from prebid-config.js."); */
        refreshPubAds();
      });
    });
  }
  /* console.log("Exiting sendAdserverRequest()."); */
}
 
(function ($) {
  $(document).ready(function () {
    // Read in mm_dfp variables passed from php to js.
    var mmDfp = Drupal.settings.mm_dfp || [];
    var dfpSlots = mmDfp.slots || [];
    var dfpLen = dfpSlots.length || 0;
 
    /* ======== Prebid.JS Config Section START ======== */
 
    // Read in mm_prebid variables passed from php to js.
    var pbjsElems = Drupal.settings.mm_prebid.headerbidElems.prebidjs || [];
    var pbjsLen = pbjsElems.length || 0;
    // Rubicon has additional config parameters.
    var rubiconConfig = Drupal.settings.mm_prebid.headerbidElems.extra_config.prebidjs.rubicon || [];
 
    // Loop thru each Prebid.JS id and grab slot properties from mm_dfp.
    for (var a = 0; a < pbjsLen; a++) {
      // If the dfp id is found in the DOM, then add the slot info to
      // the `pbjsUnits`.
      if (document.getElementById(pbjsElems[a].dfpId)) {
        // Search mm_dfp slots for an id that matches the prebid id.
        if (pbjsElems[a].dfpId == 'jwplayer-container-1') {
          /* console.log("Setting up videoAdUnit."); */
          // The jwplayer id was found on the page, so prepare pbjsVideoUnits.
          // Configure autoplay based on shouldAutoplay() in jwplayer.tpl.php.
          if (shouldAutoplay()) {
            autoplayParam = 'auto_play_sound_on';
          }
          else {
            autoplayParam = 'click_to_play';
          }
 
          // Define the video ad unit parameters.
          videoAdUnit = {
            code: "/1006215/OoyalaPreRoll",
            mediaTypes: {
              video: {
                context: 'instream',
                playerSize: [640,480]
              }
            },
            bids: [{
              bidder: 'appnexus',
              params: {
                placementId: pbjsElems[a].placementIds.appnexus,
                video: {
                  skippable: true,
                  playback_methods: [autoplayParam]
                }
              }
            }]
          };
          pbjsVideoUnits.push(videoAdUnit);
        }
        else {
          // Otherwise, loop thru traditional placement ids.
          for (var d = 0; d < dfpLen; d++) {
            if (dfpSlots[d].element === pbjsElems[a].dfpId) {
              var pbjsBids = [];
              // Loop thru the element's bidders to set up placement ids.
              for (var network in pbjsElems[a].placementIds) {
                if (pbjsElems[a].placementIds.hasOwnProperty(network)) {
                  // This is javascript's way of doing "foreach()".
                  if (network == 'ix') {
                    // With IndexExchange, each ad size must be added as a unique
                    // bidder entry.
                    for (var s = 0; s < dfpSlots[d].sizes.length; s++) {
                      var pbjsParams = {
                        siteId: pbjsElems[a].placementIds[network],
                        size: dfpSlots[d].sizes[s]
                      }
                      pbjsBids.push({
                        bidder: network,
                        params: pbjsParams
                      });
                    }
                  }
                  else {
                    // Just add one bidder entry for other ad newtorks.
                    if (network == 'appnexus') {
                      var pbjsParams = {
                        placementId: pbjsElems[a].placementIds[network]
                      }
                    }
                    else if (network == 'rubicon') {
                      var pbjsParams = {
                        accountId: rubiconConfig.account_id,
                        siteId: rubiconConfig.site_id,
                        zoneId: pbjsElems[a].placementIds[network]
                      }
                    }
                    pbjsBids.push({
                      bidder: network,
                      params: pbjsParams
                    });
                  }
                }
              }
              // Add slot info to `pbjsUnits` for Prebid.JS.
              pbjsUnits.push({
                // Example: '12345/box-1'.
                code: dfpSlots[d].slot,
                mediaTypes: {
                  banner: {
                    // Example: [[300,250], [300,600]].
                    sizes: dfpSlots[d].sizes
                  }
                },
                bids: pbjsBids
              });
              break;
            }
          }
        }
      }
    }
 
    setTimeout(function () {
      /* console.log("Prebid Timeout reached. Calling sendAdServerRequest()."); */
      sendAdserverRequest();
    }, PREBID_TIMEOUT);
 
    /* ======== Prebid.js (AppNexus) Config Section END ======== */
 
    /* ======= Prebid.js Boilerplate Section START. No Need to Edit. ======= */
 
    if (pbjsUnits.length > 0 || pbjsVideoUnits.length > 0) {
      // Only request bids if Prebid.JS units are present.
      /* console.log("PBJS slots found. Calling disableInitialLoading()."); */
      disableInitialLoading();
 
      // Special handling for jwplayer ad units.
      if (pbjsVideoUnits.length > 0) {
        pbjs.que.push(function () {
          /* console.log("  pbjsVideoUnits found. Adding pbjsVideoUnits to the pbjs que."); */
          pbjs.setConfig({
            cache: {
              url: 'https://prebid.adnxs.com/pbc/v1/cache'
            },
            mediaTypePriceGranularity: {
              // Set a custom price granularity for video ads.
              video: {
                buckets: [{
                  precision: 2,
                  min: 10,
                  max: 40,
                  increment: 0.1
                }]
              }
            }
          });
          /* console.log("  Calling pbjs.addAdUnits(pbjsVideoUnits)."); */
          pbjs.addAdUnits(pbjsVideoUnits);
 
          /* console.log("  Calling pbjs.requestBids() with extra video params."); */
          pbjs.requestBids({
            bidsBackHandler: function (bids) {
              var videoUrlObj = {
                adUnit: videoAdUnit,
                url: vastTag
              };
              /* console.log("    Setting videoUrl equal to pbjs.adServers.dfp.buildVideoUrl(videoUrlObj) via pbjs."); */
              videoUrl = pbjs.adServers.dfp.buildVideoUrl(videoUrlObj);
              /* console.log("    videoUrl: " + videoUrl); */
            }
          });
        });
      }
      else if (typeof invokeVideoPlayer === 'function') {
        /* console.log("  pbjsVideoUnits is empty, but jwplayer is still present."); */
        /* console.log("  Calling invokeVideoplayer() with no videoUrl from prebid-config.js."); */
        invokeVideoPlayer();
      }
 
      if (pbjsUnits.length > 0) {
        pbjs.que.push(function () {
          /* console.log("  pbjsUnits found. Calling pbjs.addAdUnits(pbjsUnits)."); */
          pbjs.addAdUnits(pbjsUnits);
        });
      }
 
      // Send the ad server request at the very end.
      pbjs.que.push(function () {
        /* console.log("  Calling pbjs.requestBids() via pbjs.que, from prebid-config.js."); */
        pbjs.requestBids({
          bidsBackHandler: sendAdserverRequest
        });
      });
    }
 
    else {
      /* console.log("No PBJS units present. Just set pbjs.adserverRequestSent to true."); */
      pbjs.adserverRequestSent = true;
    }
 
    /* ======== Prebid.js Boilerplate Section END ======== */
 
  });
}(jQuery));