Ferramenta de transição de públicos-alvo no nível da campanha

Ícone de ferramentas

Agora o Google Ads é compatível com públicos-alvo de pesquisa e do Shopping no nível da campanha. Para usar públicos-alvo anteriormente, era necessário aplicá-los a todos os grupos de anúncios da campanha. Com a ferramenta de transição de públicos-alvo no nível da campanha, você simplifica a configuração da conta ao verificar seus públicos-alvo no nível do grupo de anúncios e promover aqueles qualificados no nível da campanha.

Como funciona

O script começa lendo as informações de configuração de uma planilha do Google:

Em seguida, ele processa as campanhas da conta. Para cada campanha processada, o script começa listando cada grupo de anúncios e seus públicos-alvo. Em seguida, ele promove os públicos-alvo qualificados no nível da campanha e remove aqueles do grupo de anúncios.

Por padrão, todas as campanhas ENABLED e PAUSED da sua conta são processadas. Você pode alterar esse comportamento rotulando um subconjunto de suas campanhas com um rótulo e especificando na planilha.

O script exporta as novas segmentações por público-alvo da campanha para a guia Saída da planilha. Ela também pode enviar um e-mail com um resumo das alterações feitas.

Como os públicos-alvo são identificados para a promoção

O script pode operar em dois modos.

No modo básico, um público-alvo é considerado para promoção se:

  • Ela segmenta todos os grupos de anúncios de uma campanha.
  • O modificador de lance para o público-alvo é o mesmo para todos os grupos de anúncios dessa campanha.
  • As configurações de segmentação do grupo de anúncios ("Somente lance" ou "Segmentação e lance") são as mesmas para todos os grupos de anúncios dessa campanha.
  • O público-alvo não foi excluído no nível da campanha.

No modo avançado, um público-alvo é considerado para promoção se:

  • Ela segmenta um ou mais grupos de anúncios em uma campanha.
  • O público-alvo não foi excluído no nível da campanha.
  • Se houver grupos de anúncios sem públicos-alvo, eles só serão promovidos se todas as configurações de segmentação forem "Somente lance". Isso evita queda não intencional no tráfego nos grupos de anúncios sem públicos-alvo antes da transição.

Esse comportamento é controlado pela configuração Fazer a transição de públicos-alvo no nível do grupo de anúncios? na planilha de configuração.

  • "Não" é o modo básico
  • "Sim" é o modo avançado.

Como o modificador de lance é calculado para o público-alvo promovido

No modo básico, o modificador de lances no nível do grupo de anúncios também é usado no nível da campanha.

No modo avançado, escolha o algoritmo para calcular o modificador de lance para o público-alvo promovido. As seguintes opções estão disponíveis:

Algoritmo Significado
AVERAGE Usa a média dos modificadores de lance para todos os grupos de anúncios segmentados por esse público-alvo.
WEIGHTED_SPEND_AVERAGE (padrão) Usa a média ponderada por gasto dos modificadores de lance de todos os grupos de anúncios que esse público-alvo segmenta. São usadas as estatísticas dos últimos 30 dias. Se não houver estatísticas para nenhum dos grupos de anúncios, o método AVERAGE será usado.
WEIGHTED_IMPRESSION_AVERAGE Usa a média ponderada por impressão dos modificadores de lance para todos os grupos de anúncios que esse público-alvo segmenta. São usadas as estatísticas dos últimos 30 dias. Se não houver estatísticas para nenhum dos grupos de anúncios, o método AVERAGE será usado.

Agendamento

É possível executar esse script uma vez para fazer as alterações necessárias. Portanto, a programação não é obrigatória.

Instalação

  • Configure um script com o código-fonte abaixo. Use uma cópia desta planilha modelo.
  • Especifique a configuração do script na guia Configuração da planilha.
  • Não se esqueça de atualizar SPREADSHEET_URL no seu script.

Código-fonte

// Copyright 2016, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @name Campaign Level Audiences Transition Tool
 *
 * @overview Campaign Level Audiences Transition Tool analyzes audiences at
 *     AdGroup level, identifies potential duplicates, and transitions them to
 *     campaign level audiences. See
 *     https://developers.google.com/google-ads/scripts/docs/solutions/campaign-audience-transition-tool
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 2.0
 *
 * @changelog
 * - version 2.0
 *   - Updated to use new Google Ads scripts features.
 * - version 1.0
 *   - Released initial version.
 * - version 1.1
 *   - Performance improvements.
 *   - Proper handling of campaign-level excluded audiences.
 * - version 1.2
 *   - Better handling of potential timeouts for large campaigns.
 *   - Only process campaigns with ad group-level audiences.
 * - version 1.3
 *   - Ignore campaigns that use audiences other than remarketing lists.
 */

/**
 * Specifies the URL of the spreadsheet from which configuration is read and
 * results are exported. This should be a copy of https://goo.gl/WhrhS7.
 */
const SPREADSHEET_URL = 'INSERT_SPREADSHEET_URL';

/* Constants for accessing spreadsheet settings. */

/**
 * Name of the spreadsheet range corresponds to the PromoteUniqueAudiences
 * setting. This setting determines whether any audience found at ad group level
 * should be promoted or not.
 */
const SHOULD_PROMOTE_UNIQUE_AUDIENCES_RANGE_NAME =
    'PromoteUniqueAudiences';

/**
 * Name of the spreadsheet range that corresponds to the
 * UniqueAudienceBidModiferTechnology setting. This setting specifies how to
 * calculate the campaign level bid modifier when unique audience targeting ad
 * groups are promoted.
 */
const UNIQUE_AUDIENCE_BID_MODIFIER_TECH_RANGE_NAME =
    'UniqueAudienceBidModiferTechnology';

/**
 * Name of the spreadsheet range that corresponds to the EmailAddress setting.
 * This setting provides the list of users that the script will email upon
 * completion.
 */
const EMAIL_ADDRESS_RANGE_NAME = 'EmailAddress';

/**
 * Name of the spreadsheet range that corresponds to the CampaignLabel setting.
 * This setting determines the list of campaigns in an account that the script
 * will process.
 */
const CAMPAIGN_LABEL_RANGE_NAME = 'CampaignLabel';

const CUSTOMER_ID_RANGE_NAME = 'CustomerId';
const LAST_RUN_RANGE_NAME = 'LastRun';

const BidModifierTech = {
  Average: 'AVERAGE',
  WeightedSpendAverage: 'WEIGHTED_SPEND_AVERAGE',
  WeightedImpressionAverage: 'WEIGHTED_IMPRESSION_AVERAGE',
};

const SheetNames = {
  Output: 'Output',
  Configuration: 'Configuration'
};

/**
 * Configuration to be used for running reports.
 */
const REPORTING_OPTIONS = {
  // Comment out the following line to default to the latest reporting version.
  apiVersion: 'v11'
};

/**
 * The main method.
 */
function main() {
  const spreadsheetOutput = [];

  const spreadsheet = validateSpreadsheet();
  const outputSheet = loadOutputsheet(spreadsheet);
  const config = loadConfiguration(spreadsheet);
  ensureCampaignLabelExists(config);

  const campaignMap = getCampaigns(config);

  let processedCount = 0;
  for (const campaignId in campaignMap) {
    try {
      processCampaign(campaignId, campaignMap[campaignId], config,
          spreadsheetOutput);
      processedCount++;
    } catch (err) {
      console.error(err);
    }

    // Stop processing if we only have 5 minutes left.
    if (AdsApp.getExecutionInfo().getRemainingTime() < 300) {
      console.warn('Less than 5 mins. of execution time remaining. ' +
          'Stopping now.');
      break;
    }
  }
  writeToSpreadsheet(spreadsheetOutput, outputSheet);
  sendEmailReport(config, processedCount);
}

function prepareAudienceLookupMap(adGroupMap, campaignDetail) {
  const audienceListLookupMap = {};
  const excludedCampaignAudienceListLookupMap = {};

  for (const adGroupId in adGroupMap) {
    const adGroupDetail = adGroupMap[adGroupId];
    for (const audienceId in adGroupDetail['Audiences']) {
      audienceListLookupMap[audienceId] = 1;
    }
  }

  for (const excludedAudienceId in campaignDetail['ExcludedAudiences']) {
    excludedCampaignAudienceListLookupMap[excludedAudienceId] = 1;
  }

  removeExcludedCriteriaFromPromotionlist(audienceListLookupMap,
      excludedCampaignAudienceListLookupMap);
  return audienceListLookupMap;
}

/**
 * Gets a campaign by its ID and channel type.
 *
 * @param {number} campaignId the campaign ID to retrieve.
 * @param {string} channelType the advertising channel type for the campaign.
 *    Search or Shopping.
 *
 * @return {Campaign|ShoppingCampaign} a campaign object.
 */
function getCampaign(campaignId, channelType) {
  let selector = null;

  switch (channelType) {
    case 'SEARCH':
      selector = AdsApp.campaigns();
      break;
    case 'SHOPPING':
      selector = AdsApp.shoppingCampaigns();
      break;
    default:
      return null;
  }

  return selector.withIds([campaignId]).get().next();
}

/**
 * Gets the new bid modifier for an audience.
 *
 * @param {Object<string, Object>} adGroupMap the map with key as ad group ID,
 *      and value as ad group details.
 * @param {number} audienceId the audience ID.
 * @param {Object} config the configuration object loaded from spreadsheet.
 *
 * @return {number} the new bid modifier.
 */
function getNewBidModifierForAudience(adGroupMap, audienceId, config) {
  let bidModifier = 0;

  if (config[SHOULD_PROMOTE_UNIQUE_AUDIENCES_RANGE_NAME]) {
    switch (config[UNIQUE_AUDIENCE_BID_MODIFIER_TECH_RANGE_NAME]) {
      case BidModifierTech.Average:
        bidModifier = getAverageBidModifier(adGroupMap, audienceId);
        break;
      case BidModifierTech.WeightedSpendAverage:
        bidModifier = getWeightedStatBidModifier(adGroupMap,
            audienceId, 'metrics.cost_micros');
        break;
      case BidModifierTech.WeightedImpressionAverage:
        bidModifier = getWeightedStatBidModifier(adGroupMap,
            audienceId, 'metrics.impressions');
        break;
      default:
        throw new Error(`Unknown value for ` +
            `${UNIQUE_AUDIENCE_BID_MODIFIER_TECH_RANGE_NAME} : ` +
            `"${config[UNIQUE_AUDIENCE_BID_MODIFIER_TECH_RANGE_NAME]}".`);
    }
  } else {
    bidModifier = getMatchingBidModifier(adGroupMap, audienceId);
  }
  return bidModifier;
}

/**
 * Gets a selector for audiences.
 *
 * @param {string} channelType the advertising channel type for the campaign.
 *    Search or Shopping.
 * @param {number} campaignId the campaign ID to retrieve audiences from.
 *
 * @return {Selector} a selector for getting the audiences.
 */
function getAudienceSelector(channelType, campaignId) {
  let selector = null;

  switch (channelType) {
    case 'SEARCH':
      selector = AdsApp.adGroupTargeting();
      break;
    case 'SHOPPING':
      selector = AdsApp.shoppingAdGroupTargeting();
      break;
    default:
      return null;
  }
  selector = selector.audiences()
    .withCondition(`ad_group_criterion.status IN ('ENABLED', 'PAUSED')`)
    .withCondition(`campaign.id = ${campaignId}`)
    .withCondition(`ad_group.status IN ('ENABLED','PAUSED')`)
    .withLimit(500000);
  return selector;
}

/**
 * Removes the promoted audiences from the ad group.
 *
 * @param {number} campaignId the campaign ID to remove promoted audiences
 *    from.
 * @param {string} channelType the advertising channel type for the campaign.
 *    Search or Shopping.
 */
function removeAudiencesFromAdGroup(campaignId, channelType) {
  for (const audience of getAudienceSelector(channelType, campaignId)) {
    audience.remove();
  }

  // This is a temporary workaround to clear any batch caching so
  // ad group level removes are not combined with campaign level additions.
  getAudienceSelector(channelType, campaignId).get();
}

/**
 * Add promoted audiences to a campaign.
 *
 * @param {Object<string, number>} promotedAudienceMap a map with key as the
 *      audience ID, and value as the proposed bid modifier for that audience.
 * @param {Campaign} campaign the campaign to which new audiences are added.
 *
 * @return {!Array.<Array.<string>>} the output details of promoted audiences,
 *      to be appended to the spreadsheet.
 */
function addPromotedAudiencesToCampaign(promotedAudienceMap, campaign) {
  const spreadsheetOutput = [];

  for (let audienceId in promotedAudienceMap) {
    // Add campaign level criteria.
    const bidModifier = promotedAudienceMap[audienceId];

    campaign.targeting()
        .newUserListBuilder()
        .withAudienceId(audienceId)
        .withBidModifier(bidModifier)
        .build();

    spreadsheetOutput.push([campaign.getName(), audienceId, bidModifier]);
  }
  return spreadsheetOutput;
}

function appendSpreadsheetOutput(promotedAudiencesDetails,
                  spreadsheetOutput, targetSetting) {
  for (const audienceDetail of promotedAudiencesDetails) {
    audienceDetail.push(targetSetting);
    spreadsheetOutput.push(audienceDetail);
  }
}

/**
 * Gets the audience list for promotion.
 *
 * @param {Object<string, Object>} adGroupMap the map with key as ad group ID,
 *      and value as ad group details.
 * @param {Object} config the configuration object loaded from spreadsheet.
 * @param {Object} campaignDetail details of the campaign that is being
 *      processed.
 *
 * @return {!Object<string, number>} a map with key as the audience ID to be
 *      promoted, and value as its proposed bid modifier.
 */
function getAudienceListForPromotion(adGroupMap, config, campaignDetail) {
  const audienceListLookupMap = prepareAudienceLookupMap(
      adGroupMap, campaignDetail);

  let promotingAudienceList = [];
  if (config[SHOULD_PROMOTE_UNIQUE_AUDIENCES_RANGE_NAME]) {
    // We want to promote all the remaining audiences in
    // audienceListLookupMap (including audiences that do not exist
    // in all ad groups).
    promotingAudienceList = Object.keys(audienceListLookupMap);
  } else {
    // Skip all audiences that are missing in one or more ad groups.
    promotingAudienceList = getAudienceListForConservativePromotion(
      audienceListLookupMap, adGroupMap);
  }

  const promotedAudienceMap = {};

  if (promotingAudienceList.length !=
      Object.keys(audienceListLookupMap).length) {
    console.warn('Not all ad group level audiences can be promoted. ' +
        'Skipping this campaign.');
  } else {
    for (const audienceId of promotingAudienceList) {
      const bidModifier = getNewBidModifierForAudience(adGroupMap,
                                                     audienceId, config);

      // We could not find a valid bid modifier, so we won't promote this
      // audience.
      if (!bidModifier) {
        continue;
      }

      promotedAudienceMap[audienceId] = bidModifier;
    }
  }
  return promotedAudienceMap;
}

/**
 * Check and pause a campaign.
 *
 * @param {Campaign} campaign the campaign to be paused.
 *
 * @return {boolean} true if the campaign was paused, false otherwise.
 */
function checkAndPauseCampaign(campaign) {
  if (campaign.isEnabled()) {
    campaign.pause();
    return true;
  }
  return false;
}

/**
 * Updates the campaign settings after promotion.
 *
 * @param {Object<string, boolean>} promotedAudienceMap map with key as an
 *      audience ID to promote, and value as a boolean.
 * @param {Object} config the configuration object loaded from the spreadsheet.
 * @param {boolean} enableCampaign true if the campaign should be enabled,
 *      false otherwise.
 * @param {string} targetSetting the campaign's new target setting for
 *      user lists.
 * @param {Campaign} campaign the campaign to be updated.
 */
function updateCampaignSettingsAfterPromotion(promotedAudienceMap, config,
    enableCampaign, targetSetting, campaign) {
  if (Object.keys(promotedAudienceMap).length > 0) {
    // Set the campaign's target setting.
    campaign.targeting().setTargetingSetting('USER_INTEREST_AND_LIST',
                                             targetSetting);
  }

  // The campaign is ready to serve. Enable it if we had paused it earlier.
  if (enableCampaign) {
    campaign.enable();
  }
}

/**
 * Processes a campaign.
 *
 * @param {number} campaignId the campaign ID.
 * @param {Object} campaignDetail the campaign details object.
 * @param {Object} config the configuration object.
 * @param {Array.<Array.<string>>} spreadsheetOutput the contents to be written
 *     to the output sheet.
 */
function processCampaign(campaignId, campaignDetail, config,
    spreadsheetOutput) {
  console.log(`Processing campaign "${campaignDetail['Name']}".`);

  // Skip if there are no ad group-level audiences.
  if (campaignDetail['AdGroupAudiences'] === 0) {
    console.log(`Campaign "${campaignDetail['Name']}" has no ad group-level ` +
               `audiences.`);
    return;
  }

  // Skip if there are too many audiences to process due to limitations.
  if (campaignDetail['AdGroupAudiences'] > 200000) {
    console.log(`Campaign "${campaignDetail['Name']}" is too large to process.`);
    return;
  }

  getAdGroups(campaignDetail, campaignId);

  // Skip if there other audiences besides remarketing lists.
  if (campaignDetail['UsesOtherAudiences'] === true) {
    console.log(`Campaign "${campaignDetail['Name']}" has audiences besides ` +
               `remarketing and is being skipped.`);
    return;
  }

  populateCampaignExcludedAudienceDetails(campaignDetail, campaignId);

  const channelType = campaignDetail['ChannelType'];
  const adGroupMap = campaignDetail['AdGroups'];

  // 1. Get the targeting setting for the ad groups.
  const targetSetting = getTargetSettingForAdGroups(adGroupMap);

  if (!targetSetting) {
    console.warn(`Target setting of all ad groups don't match. Skipping ` +
               `this campaign.`);
    return;
  }

  const promotedAudienceMap = getAudienceListForPromotion(adGroupMap, config,
      campaignDetail);

  // Skip if there isn't enough time left based on campaign size. Using 7,500
  // operations per minute as the approximation.
  const estimatedTimeToProcessCampaign = campaignDetail['AdGroupAudiences']
      / (7500 / 60);
  if (estimatedTimeToProcessCampaign >=
      AdsApp.getExecutionInfo().getRemainingTime()) {
    console.warn(`Not enough time remaining to process campaign ` +
                 `"${campaignDetail['Name']}".`);
    return;
  }

  if (Object.keys(promotedAudienceMap).length === 0) {
    // Pause the campaign. This is to prevent the campaign serving to the wrong
    // audience or serving without bid adjustments during the changes.
    const campaign = getCampaign(campaignId, channelType);
    const enableCampaign = checkAndPauseCampaign(campaign);

    removeAudiencesFromAdGroup(campaignId, channelType);
    const promotedAudiencesDetails =
        addPromotedAudiencesToCampaign(promotedAudienceMap, campaign);
    appendSpreadsheetOutput(promotedAudiencesDetails,
                            spreadsheetOutput, targetSetting);

    updateCampaignSettingsAfterPromotion(promotedAudienceMap, config,
        enableCampaign, targetSetting, campaign);
  }
}

/**
 * Sends an email report.
 *
 * @param {Object} config the configuration object.
 * @param {number} processedCount the number of campaigns processed.
 */
function sendEmailReport(config, processedCount) {
  if (config[EMAIL_ADDRESS_RANGE_NAME]) {
    const customerId = AdsApp.currentAccount().getCustomerId();
    const message = [];
    message.push(`<p>Hi,</p>`,
                 `<p>Campaign level Audiences Transition tool processed <b>` +
                 `${processedCount}` +
                 `</b> campaigns for customer ID: <b>${customerId}` +
                 `</b>. See <a href=" ${SPREADSHEET_URL} ` +
                 `">the output spreadsheet</a> for detailed report.</p>`,
                 `<p>Cheers,<br />Google Ads Scripts Team.</p>`
                 );

    MailApp.sendEmail({
      to: config[EMAIL_ADDRESS_RANGE_NAME],
      subject: `Campaign level Audiences Transition tool (${customerId})`,
      htmlBody: message.join('\n'),
    });
  }
}

/**
 * Writes output and metadata to spreadsheet.
 *
 * @param {Array.<Array.<string>>} spreadsheetOutput A rectangular array of data
 *     to be written to the output sheet.
 * @param {Sheet} outputSheet the output sheet.
 */
function writeToSpreadsheet(spreadsheetOutput, outputSheet) {
  if (spreadsheetOutput.length > 0) {
    const lastRow = outputSheet.getLastRow();
    outputSheet.insertRowsAfter(outputSheet.getMaxRows(),
        spreadsheetOutput.length);
    outputSheet.getRange(lastRow + 1, 1, spreadsheetOutput.length, 4)
        .setValues(spreadsheetOutput);
  }
  const spreadsheet = outputSheet.getParent();
  spreadsheet.getRangeByName(CUSTOMER_ID_RANGE_NAME).setValue(
      AdsApp.currentAccount().getCustomerId());
  spreadsheet.getRangeByName(LAST_RUN_RANGE_NAME).setValue(new Date());
}

/**
 * Ensure that the spreadsheet is valid.
 *
 * @return {Sheet} the output sheet within the spreadsheet.
 *
 * @throws {Error} if spreadsheet is missing, or the Output sheet is missing.
 */
function validateSpreadsheet() {
  // Get spreadsheet by ID.
  let spreadsheet = null;
  try {
    spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  } catch (err) {
    console.error(err);
  }

  if (!spreadsheet) {
    throw new Error('Spreadsheet is missing or unavailable. Please ensure ' +
        'the URL is correct and that you have permission to edit.');
  }
  return spreadsheet;
}

/**
 * Loads the output sheet from the spreadsheet.
 *
 * @param {Spreadsheet} spreadsheet the spreadsheet object.
 *
 * @return {Sheet} the output sheet object.
 *
 * @throws {Error} if the sheet is missing.
 */
function loadOutputsheet(spreadsheet) {
  const outputSheet = spreadsheet.getSheetByName(SheetNames.Output);

  if (!outputSheet) {
    throw new Error(`A sheet named "${SheetNames.Output}" is missing ` +
        `in the spreadsheet. Make sure you create the spreadsheet as a ` +
        `copy of the template spreadsheet.`);
  }
  return outputSheet;
}

/**
 * Reads a setting from the spreadsheet.
 *
 * @param {Spreadsheet} spreadsheet the spreadsheet object.
 * @param {string} configName the setting name.
 *
 * @return {string} the setting value.
 */
function readSetting(spreadsheet, configName) {
  return spreadsheet.getRangeByName(configName).getValue();
}

/**
 * Loads configuration from the spreadsheet.
 *
 * @param {Spreadsheet} spreadsheet the spreadsheet object.
 *
 * @return {!Object} the configuration object from the spreadsheet.
 *
 * @throws {Error} if the configuration cannot be read.
 */
function loadConfiguration(spreadsheet) {
  let config = null;
  try {
    config = {};
    config[UNIQUE_AUDIENCE_BID_MODIFIER_TECH_RANGE_NAME] =
        readSetting(spreadsheet, UNIQUE_AUDIENCE_BID_MODIFIER_TECH_RANGE_NAME);
    config[SHOULD_PROMOTE_UNIQUE_AUDIENCES_RANGE_NAME] =
        readSetting(spreadsheet,
            SHOULD_PROMOTE_UNIQUE_AUDIENCES_RANGE_NAME) === 'Yes';
    config[CAMPAIGN_LABEL_RANGE_NAME] =
        readSetting(spreadsheet, CAMPAIGN_LABEL_RANGE_NAME);
    config[EMAIL_ADDRESS_RANGE_NAME] =
        readSetting(spreadsheet, EMAIL_ADDRESS_RANGE_NAME);
  } catch (err) {
    console.error(err);
  }

  if (!config) {
    throw new Error('Failed to read configuration from spreadsheet. Make ' +
        'sure you create the spreadsheet as a copy of the template ' +
        'spreadsheet.');
  }

  return config;
}

/**
 * Ensure that campaign label exists in the account if specified.
 *
 * @param {Object} config the configuration object from the spreadsheet.
 *
 * @throws {Error} if campaign label is specified, but missing.
 */
function ensureCampaignLabelExists(config) {
  if (config[CAMPAIGN_LABEL_RANGE_NAME]) {
    const campaignLabelCount = AdsApp.labels()
    .withCondition(`label.name = "${config[CAMPAIGN_LABEL_RANGE_NAME]}"`)
    .get().totalNumEntities();

    if (campaignLabelCount == 0) {
      throw `Label named "${config[CAMPAIGN_LABEL_RANGE_NAME]}"` +
          ` is missing. Please check the campaign label setting in the ` +
          `spreadsheet.`;
    }
  }
}

/**
 * Gets the matching bid modifier for a given audience ID when it targets a set
 * of ad groups.
 *
 * @param {Object<number, Object>} adGroupLookupMap A map that stores the
 *     details of all the ad groups. The key is the ad group ID, and value is an
 *     object that stores the ad group details.
 * @param {number} audienceId The ID of the audience.
 *
 * @return {number} the Bid modifier for the audience.
 * @throws {Error} if the audience targeting is missing in the ad group, or
 *     if the adGroupLookupMap is empty.
 */
function getMatchingBidModifier(adGroupLookupMap, audienceId) {
  let bidModifier = 0;
  for (const adGroupId in adGroupLookupMap) {
    if (audienceId in adGroupLookupMap[adGroupId]['Audiences']) {
      const searchAudience =
          adGroupLookupMap[adGroupId]['Audiences'][audienceId];

      if (bidModifier == 0) {
        bidModifier = searchAudience['BidModifier'];
      } else if (searchAudience['BidModifier'] !== bidModifier) {
        return 0;
      }
    } else {
      throw new Error(Utilities.formatString(`AdGroup ID: ${adGroupId} ` +
          `doesn't target Audience ID: ${audienceId},`+
          `so no matching bid modifier was found.`));
    }
  }
  return bidModifier;
}

/**
 * Gets the bid modifier for a given audience ID using the weighted stats of a
 * given set of ad groups it targets.
 *
 * @param {Object<number, Object>} adGroupLookupMap A map that stores the
 *     details of all the ad groups. The key is the ad group ID, and value is an
 *     object that stores the ad group details.
 * @param {number} audienceId The ID of the audience.
 * @param {string} statName The name of the stat to use for calculating
 *     weighted averages.
 *
 * @return {number} the Bid modifier for the audience.
 * @throws {Error} if the calculated bid modifier is zero, or the totals of the
 *      stats is zero.
 */
function getWeightedStatBidModifier(adGroupLookupMap, audienceId,
    statName) {

  let bidModifier = 0;
  let totalStats = 0;

  for (const adGroupId in adGroupLookupMap) {
    const adGroupDetails = adGroupLookupMap[adGroupId];
    // Since we pick unique audience ID, this audience may not be targeting
    // ad group ID.
    if (audienceId in adGroupLookupMap[adGroupId]['Audiences']) {
      const searchAudience =
          adGroupLookupMap[adGroupId]['Audiences'][audienceId];
      const stat = parseFloat(adGroupDetails[statName]);
      bidModifier += searchAudience['BidModifier'] * stat;
      totalStats += stat;
    }
  }

  if (bidModifier == 0 || totalStats == 0) {
    console.log(Utilities.formatString(`No stats found for audience ID: ` +
        `${audienceId}. ` +
        `Using average bid modifier.`));
    return getAverageBidModifier(adGroupLookupMap, audienceId);
  }

  return Number((bidModifier / totalStats).toFixed(2));
}

/**
 * Gets the average bid modifier for a given audience ID for a given set of
 * ad groups it targets.
 *
 * @param {Object<number, Object>} adGroupLookupMap A map that stores the
 *     details of all the ad groups. The key is the ad group ID, and value is
 *     an object that stores the ad group details.
 * @param {number} audienceId The ID of the audience.
 *
 * @return {number} the Bid modifier for the audience.
 */
function getAverageBidModifier(adGroupLookupMap, audienceId) {
  let bidModifierSum = 0;
  let count = 0;

  for (const adGroupId in adGroupLookupMap) {
    // Since we pick unique audience ID, this audience may not be targeting
    // ad group ID.
    if (audienceId in adGroupLookupMap[adGroupId]['Audiences']) {
      const searchAudience =
          adGroupLookupMap[adGroupId]['Audiences'][audienceId];
      const bidModifier = searchAudience['BidModifier'];
      bidModifierSum += bidModifier;
      count++;
    }
  }

  if (count == 0 || bidModifierSum == 0) {
    console.log(Utilities.formatString(`Bid modifier for audience ID: `+
        `${audienceId} is zero.`));
    return 0;
  }

  // Round off bid modifier to 2 places.
  return Number((bidModifierSum / count).toFixed(2));
}

/**
 * Removes excluded criteria from promotion list.
 *
 * @param {Object<number, bool>} audienceListLookupMap The map to store all the
 *     audiences found so far.
 * @param {Object<number, bool>} excludedCampaignAudienceListLookupMap The map
 *     to store all the campaign level excluded audiences found so far.
 */
function removeExcludedCriteriaFromPromotionlist(audienceListLookupMap,
    excludedCampaignAudienceListLookupMap) {
  // Remove all the excluded audiences from the promotion list.

  for (excludedAudienceId in excludedCampaignAudienceListLookupMap) {
    if (excludedAudienceId in audienceListLookupMap) {
      console.log(`--Audience ID: ` +
          `${excludedAudienceId}` +
          ` is excluded on the campaign, removing from promotion list.`);
      delete audienceListLookupMap[excludedAudienceId];
    }
  }
}

/**
 * Gets the common target setting for a list of ad groups.
 *
 * @param {Object<number, Object>} adGroupMap The map of ad groups being
 *     processed.
 *
 * @return {string} The common target setting, or null if no common target
 *     setting can be found.
 */
function getTargetSettingForAdGroups(adGroupMap) {
  // 1. See if all the ad groups have the same targeting setting.
  let targetSetting = null;
  let blankAdGroupCount = 0;
  let targetSettingMatches = true;
  for (adGroupId in adGroupMap) {

    const adGroupDetails = adGroupMap[adGroupId];

    // Skip checking the target settings for ad groups that have no search
    // audience criteria in them.
    const numCriteria = Object.keys(adGroupDetails['Audiences']).length;

    if (numCriteria == 0) {
      console.warn(`--Adgroup "${adGroupDetails['Name']}" has no search ` +
          `audience criteria, skipping...`,);
      blankAdGroupCount++;
      continue;
    }

    const tempTargetSetting = adGroupDetails['TargetingSetting'];

    if (!targetSetting) {
      targetSetting = tempTargetSetting;
    } else if (targetSetting !== tempTargetSetting) {
      targetSettingMatches = false;
      break;
    }
  }

  // Empty ad groups are skipped over only if the overall target setting
  // is "Bid Only".
  if (blankAdGroupCount != 0 && targetSetting !== 'TARGET_ALL_TRUE') {
    targetSettingMatches = false;
  }

  if (targetSettingMatches) {
    return targetSetting;
  } else {
    return null;
  }
}

/**
 * Gets the audience list for conservative promotion.
 *
 * @param {Object<number, bool>} audienceListLookupMap The map to keep track
 *     of all the audience IDs.
 * @param {Object<number, Object>} adGroupLookupMap The map to keep track of
 *     all the ad groups.
 *
 * @return {!Array.<number>} The list of all the audiences to be promoted.
 */
function getAudienceListForConservativePromotion(audienceListLookupMap,
    adGroupLookupMap) {
  const promotingAudienceList = [];

  for (const audienceId in audienceListLookupMap) {
    let audienceSettingsMatch = true;

    // Since bid modifier can be from 0.1 to 10.0, let's initialize the
    // variable to 0.
    var bidModifier = 0;
    for (adGroupId in adGroupLookupMap) {
      const adGroupDetails = adGroupLookupMap[adGroupId];

      if (audienceId in adGroupDetails['Audiences']) {
        const searchAudience = adGroupDetails['Audiences'][audienceId];

        if (!bidModifier) {
          bidModifier = searchAudience['BidModifier'];
        } else {
          const newBidModifier = searchAudience['BidModifier'];
          if (bidModifier !== newBidModifier) {
            console.log(`--Audience ID ${audienceId} has a mismatch on ` +
                       `bid modifier. Removing from promotion list.`);
            audienceSettingsMatch = false;
            break;
          }
        }
      } else {
        console.log(`--Audience ID ${audienceId} is missing in ad group: `+
                   `"${adGroupDetails['Name']}". Removing from promotion list.`);
        audienceSettingsMatch = false;
        break;
      }
    }

    if (audienceSettingsMatch) {
      promotingAudienceList.push(audienceId);
    }
  }
  return promotingAudienceList;
}

/**
 * Gets the list of campaigns to be processed.
 *
 * @param {Object} config the configuration object from the spreadsheet.
 *
 * @return {!Object.<string, Object>} a map with campaign ID as the key and
 *      campaign details as the value.
 */
function getCampaigns(config) {
  // Filter on label IDs rather than names for faster performance.
  let query = 'SELECT campaign.id, campaign.advertising_channel_type, ' +
      'campaign.name FROM campaign WHERE ' +
      'campaign.status IN ("ENABLED","PAUSED") ' +
      'AND campaign.advertising_channel_type IN ("SEARCH","SHOPPING") ' +
      'AND campaign.advertising_channel_sub_type NOT IN ' +
      '("SEARCH_EXPRESS","APP_CAMPAIGN")';

  if (config[CAMPAIGN_LABEL_RANGE_NAME]) {
    const includedLabel = AdsApp.labels()
        .withCondition(`label.name="${config[CAMPAIGN_LABEL_RANGE_NAME]}"`)
        .get().next();

    const customerId = AdsApp.currentAccount().getCustomerId();

    query += ` AND campaign.labels CONTAINS ALL ` +
             `("customers/${customerId}/labels/${includedLabel.getId()}")`;
  }

  const report = AdsApp.search(query);
  const retval = {};
  for (const row of report) {
    const campaignId = row.campaign.id;
    const campaignName = row.campaign.name;
    const channelType = row.campaign.advertisingChannelType;
    const adGroupAudienceCount = getAudienceSelector(channelType, campaignId)
        .get().totalNumEntities();
    retval[campaignId] = {
      'Id': campaignId,
      'Name': campaignName,
      'ChannelType': channelType,
      'AdGroupAudiences': adGroupAudienceCount,
      'UsesOtherAudiences': false,
      'AdGroups': {
       },
       'Audiences': {
       },
       'ExcludedAudiences': {
       }
    };
  }

  return retval;
}

/**
 * Populates individual campaign objects in a map with campaign audience
 *     targeting details.
 *
 * @param {Object} campaignDetail the campaign details object.
 * @param {number} campaignId the campaign ID.
 */
function populateCampaignExcludedAudienceDetails(campaignDetail, campaignId) {
  const excludedAudiences = AdsApp.targeting().excludedAudiences()
      .withCondition(`campaign.id = ${campaignId}`).get();

  for (const excludedAudience of excludedAudiences) {
    campaignDetail['ExcludedAudiences'][excludedAudience.getAudienceId()] = 1;
  }
}

/**
 * Populates individual campaign objects in a map with ad group details.
 *
 * @param {Object} campaignDetail the campaign details object.
 * @param {number} campaignId the campaign ID.
 */
function getAdGroups(campaignDetail, campaignId) {
  const query = `SELECT campaign.id, ad_group.id, ad_group.name, ` +
        `metrics.cost_micros FROM ad_group WHERE ad_group.status ` +
        `IN ("ENABLED","PAUSED") AND campaign.id = ${campaignId} ` +
        `AND segments.date DURING LAST_30_DAYS`;

  const report = AdsApp.search(query);

  const adGroupMap = {};

  for (const row of report) {
    const adGroupId = row.ad_group.id;
    const adGroupName = row.ad_group.name;
    const cost = row.metrics.cost_micros;
    const impressions = row.metrics.impressions;

    const adGroupDetails = {
      'Id': adGroupId,
      'Name': adGroupName,
      'Audiences': {
      },
      'ExcludedAudiences': {
      },
      'Cost': cost,
      'Impressions': impressions
    };

    campaignDetail['AdGroups'][adGroupId] = adGroupDetails;
    adGroupMap[adGroupId] = adGroupDetails;
  }

  getAdGroupAudiences(campaignId, adGroupMap, campaignDetail);
}

/**
 * Populates individual ad group objects in a map with ad group audience
 *     targeting details and target settings.
 *
 * @param {number} campaignId a single campaign ID to retrieve
 *     audience data for.
 * @param {Object.<string, Object>} adGroupMap a map with key as ad group ID
 *     and value as ad group details.
 * @param {Object} campaignDetail the campaign details object.
 */
function getAdGroupAudiences(campaignId, adGroupMap, campaignDetail) {

    let targetingSetting = '';
    const query = 'SELECT campaign.id, ad_group.id, ' +
      'ad_group.targeting_setting.target_restrictions, metrics.impressions, ' +
      'metrics.cost_micros, ad_group.status, ' +
      'ad_group_criterion.criterion_id, ' +
      'ad_group_criterion.user_list.user_list, ' +
      'ad_group_criterion.user_interest.user_interest_category,' +
      'ad_group_criterion.custom_affinity.custom_affinity,' +
      'ad_group_criterion.custom_intent.custom_intent,' +
      'ad_group_criterion.bid_modifier FROM ad_group_audience_view ' +
      'WHERE ad_group_criterion.status IN ("ENABLED","PAUSED") ' +
      'AND ad_group.status IN ("ENABLED","PAUSED") ' +
      'AND segments.date DURING LAST_30_DAYS';

    const report = AdsApp.search(query, REPORTING_OPTIONS);

    for (const row of report) {
      const adGroupId = row.ad_group.id;
      const id = row.ad_group_criterion.criterion_id;
      const criteria = row.campaign_criterion.keyword.text;
      let bidModifier = row.ad_group_criterion.bid_modifier;
      const impressions = row.metrics.impressions;
      const cost = row.metrics.cost_micros;
      const isRestrict = row.ad_group.targeting_setting.target_restrictions;

      if (isRestrict === 'true') {
        targetingSetting = 'TARGET_ALL_FALSE';
      } else {
        targetingSetting = 'TARGET_ALL_TRUE';
      }

      if (adGroupId in adGroupMap) {
        adGroupMap[adGroupId]['Audiences'][criteria] = {
          'Id': id,
          'Audience': criteria,
          'BidModifier': bidModifier,
          'Impressions': impressions,
          'Cost': cost
        };
        if (!adGroupMap[adGroupId]['TargetingSetting']) {
          adGroupMap[adGroupId]['TargetingSetting'] = targetingSetting;
        }
      }
    }
}