Google Ads 现在支持在广告系列一级设置搜索广告系列和购物广告系列受众群体。 如果之前要使用受众群体,则需要将其应用于广告系列中的每个广告组。利用广告系列级受众群体转换工具,您可以在广告组一级扫描受众群体并将符合条件的受众群体提升到广告系列一级,从而简化帐号设置。
运作方式
该脚本首先从 Google 电子表格中读取配置信息:
然后处理该帐号中的广告系列。对于处理的每个广告系列,脚本首先列出每个广告组及其目标受众群体。然后,它会将符合条件的受众群体提升到广告系列一级,并移除广告组受众群体。
默认情况下,您帐号中的所有 ENABLED
和 PAUSED
广告系列都会得到处理。您可以通过使用标签为一部分广告系列添加标签,并在电子表格中指定广告系列,从而更改此行为。
该脚本会将新的广告系列受众群体定位条件导出到电子表格的输出标签页。它还可以向您发送电子邮件,概述所做的更改。
如何确定受众群体以进行宣传
该脚本可以在两种模式下运行。
在基本模式下,符合以下条件时,受众群体才有机会宣传:
- 定位广告系列中的所有广告组。
- 该广告系列内的所有广告组均采用同一受众群体的出价调节系数。
- 该广告系列中的所有广告组的广告组定位设置(“仅出价”或“定位和出价”)都相同。
- 未在广告系列一级排除受众群体。
在高级模式下,符合以下条件时,受众群体会被视为宣传对象:
- 定位广告系列中的一个或多个广告组。
- 未在广告系列一级排除受众群体。
- 如果有任何广告组没有受众群体,则只有在定位设置均为“仅出价”时,系统才会宣传这些受众群体。这是为了防止在转换之前,没有受众群体的广告组意外出现流量下降。
此行为受配置电子表格中的“要在广告组级别改用任何受众群体吗?”设置控制。
- “否”表示基本模式
- “是”是高级模式
如何针对推广的受众群体计算出价调节系数
在基本模式下,广告组级的出价调节系数也将用于广告系列级。
在高级模式中,您可以选择使用某种算法为提升后的受众群体计算出价调节系数。可供选择的选项如下:
算法 | 含义 |
---|---|
AVERAGE |
使用此受众群体定位的所有广告组出价调节系数的平均值。 |
WEIGHTED_SPEND_AVERAGE (默认) |
使用此受众群体定位的所有广告组的出价调节系数支出加权平均值。使用过去 30 天的统计信息。如果任何广告组都没有统计信息,则使用 AVERAGE 方法。 |
WEIGHTED_IMPRESSION_AVERAGE |
对此受众群体定位的所有广告组使用出价调节系数展示次数的加权平均值。使用过去 30 天的统计信息。如果任何广告组都没有统计信息,则使用 AVERAGE 方法。 |
正在安排
您可以运行此脚本一次以进行必要的更改,因此无需安排运行时间。
初始设置
- 使用下面的源代码设置脚本。使用此电子表格模板的副本。
- 通过电子表格的配置标签页指定脚本配置。
- 切勿忘记在您的脚本中更新
SPREADSHEET_URL
。
源代码
// 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; } } } }