-
-
Save BrainlabsDigital/b06e6a175452b5b284498f88636aa29a to your computer and use it in GitHub Desktop.
| /** | |
| * | |
| * In-market Audiences Bidding | |
| * | |
| * Automatically apply modifiers to your in-market audiences based on performance. | |
| * | |
| * Version: 1.0 | |
| * Google AdWords Script maintained on brainlabsdigital.com | |
| * | |
| **/ | |
| // Use this to determine the relevant date range for your data. | |
| // See here for the possible options: | |
| // https://developers.google.com/google-ads/scripts/docs/reference/adwordsapp/adwordsapp_campaignselector#forDateRange_1 | |
| var DATE_RANGE = 'LAST_30_DAYS'; | |
| // Use this to determine the minimum number of impressions a campaign or | |
| // and ad group should have before being considered. | |
| var MINIMUM_IMPRESSIONS = 0; | |
| // Use this if you want to exclude some campaigns. Case insensitive. | |
| // For example ["Brand"] would ignore any campaigns with 'brand' in the name, | |
| // while ["Brand","Competitor"] would ignore any campaigns with 'brand' or | |
| // 'competitor' in the name. | |
| // Leave as [] to not exclude any campaigns. | |
| var CAMPAIGN_NAME_DOES_NOT_CONTAIN = []; | |
| // Use this if you only want to look at some campaigns. Case insensitive. | |
| // For example ["Brand"] would only look at campaigns with 'brand' in the name, | |
| // while ["Brand","Generic"] would only look at campaigns with 'brand' or 'generic' | |
| // in the name. | |
| // Leave as [] to include all campaigns. | |
| var CAMPAIGN_NAME_CONTAINS = []; | |
| var AUDIENCE_MAPPING_CSV_DOWNLOAD_URL = 'https://developers.google.com/adwords/api/docs/appendix/in-market_categories.csv'; | |
| function main() { | |
| Logger.log('Getting audience mapping'); | |
| var audienceMapping = | |
| getInMarketAudienceMapping(AUDIENCE_MAPPING_CSV_DOWNLOAD_URL); | |
| Logger.log('Getting campaign performance'); | |
| var campaignPerformance = getCampaignPerformance(); | |
| Logger.log('Getting ad group performance'); | |
| var adGroupPerformance = getAdGroupPerformance(); | |
| Logger.log('Making operations'); | |
| var operations = makeAllOperations( | |
| audienceMapping, | |
| campaignPerformance, | |
| adGroupPerformance | |
| ); | |
| Logger.log('Applying bids'); | |
| applyBids(operations); | |
| } | |
| function getInMarketAudienceMapping(downloadCsvUrl) { | |
| var csv = Utilities.parseCsv( | |
| UrlFetchApp.fetch(downloadCsvUrl).getContentText() | |
| ); | |
| var headers = csv[0]; | |
| var indexOfId = headers.indexOf('Criterion ID'); | |
| var indexOfName = headers.indexOf('Category'); | |
| if ((indexOfId === -1) || (indexOfName === -1)) { | |
| throw new Error('The audience CSV does not have the expected headers'); | |
| } | |
| var mapping = {}; | |
| for (var i = 1; i < csv.length; i++) { | |
| var row = csv[i]; | |
| mapping[row[indexOfId]] = row[indexOfName]; | |
| } | |
| return mapping; | |
| } | |
| function getCampaignPerformance() { | |
| return getEntityPerformance('CampaignId', 'CAMPAIGN_PERFORMANCE_REPORT'); | |
| } | |
| function getAdGroupPerformance() { | |
| return getEntityPerformance('AdGroupId', 'ADGROUP_PERFORMANCE_REPORT'); | |
| } | |
| function getEntityPerformance(entityIdFieldName, reportName) { | |
| var performance = {}; | |
| var query = "SELECT " + entityIdFieldName + ", CostPerAllConversion " + | |
| "FROM " + reportName + " " + | |
| "WHERE Impressions > " + String(MINIMUM_IMPRESSIONS) + " " + | |
| "DURING " + DATE_RANGE; | |
| var rows = AdsApp.report(query).rows(); | |
| while (rows.hasNext()) { | |
| var row = rows.next(); | |
| performance[row[entityIdFieldName]] = row.CostPerAllConversion; | |
| } | |
| return performance; | |
| } | |
| function makeAllOperations( | |
| audienceMapping, | |
| campaignPerformance, | |
| adGroupPerformance | |
| ) { | |
| var operations = []; | |
| var allCampaigns = | |
| filterCampaignsBasedOnName(AdWordsApp.campaigns()); | |
| var filteredCampaigns = | |
| filterEntitiesBasedOnDateAndImpressions(allCampaigns) | |
| .get(); | |
| while (filteredCampaigns.hasNext()) { | |
| var campaign = filteredCampaigns.next(); | |
| // Can't have both ad-group-level and campaign-level | |
| // audiences on any given campaign. | |
| if (campaignHasAnyCampaignLevelAudiences(campaign)) { | |
| var operationsFromCampaign = makeOperationsFromEntity( | |
| campaign, | |
| campaignPerformance[campaign.getId()], | |
| audienceMapping | |
| ); | |
| operations = operations.concat(operationsFromCampaign); | |
| } else { | |
| var adGroups = | |
| filterEntitiesBasedOnDateAndImpressions(campaign.adGroups()) | |
| .get(); | |
| while (adGroups.hasNext()) { | |
| var adGroup = adGroups.next(); | |
| var operationsFromAdGroup = makeOperationsFromEntity( | |
| adGroup, | |
| adGroupPerformance[adGroup.getId()], | |
| audienceMapping | |
| ); | |
| operations = operations.concat(operationsFromAdGroup); | |
| } | |
| } | |
| } | |
| return operations; | |
| } | |
| function filterCampaignsBasedOnName(campaigns) { | |
| CAMPAIGN_NAME_DOES_NOT_CONTAIN.forEach(function(part) { | |
| campaigns = campaigns.withCondition( | |
| "CampaignName DOES_NOT_CONTAIN_IGNORE_CASE '" + part.replace(/"/g,'\\\"') + "'" | |
| ); | |
| }); | |
| CAMPAIGN_NAME_CONTAINS.forEach(function(part) { | |
| campaigns = campaigns.withCondition( | |
| "CampaignName CONTAINS_IGNORE_CASE '" + part.replace(/"/g,'\\\"') + "'" | |
| ); | |
| }); | |
| return campaigns; | |
| } | |
| function filterEntitiesBasedOnDateAndImpressions(selector) { | |
| return selector | |
| .forDateRange(DATE_RANGE) | |
| .withCondition('Impressions > ' + String(MINIMUM_IMPRESSIONS)); | |
| } | |
| function makeOperationsFromEntity(entity, entityCpa, audienceMapping) { | |
| var entityAudiences = getAudiencesFromEntity(entity, audienceMapping); | |
| return makeOperations(entityCpa, entityAudiences); | |
| } | |
| function getAudiencesFromEntity(entity, audienceMapping) { | |
| var inMarketIds = Object.keys(audienceMapping); | |
| var allAudiences = entity | |
| .targeting() | |
| .audiences() | |
| .forDateRange(DATE_RANGE) | |
| .withCondition('Impressions > ' + String(MINIMUM_IMPRESSIONS)) | |
| .get(); | |
| var inMarketAudiences = []; | |
| while (allAudiences.hasNext()) { | |
| var audience = allAudiences.next(); | |
| if (isAudienceInMarketAudience(audience, inMarketIds)) { | |
| inMarketAudiences.push(audience); | |
| } | |
| } | |
| return inMarketAudiences; | |
| } | |
| function isAudienceInMarketAudience(audience, inMarketIds) { | |
| return inMarketIds.indexOf(audience.getAudienceId()) > -1; | |
| } | |
| function makeOperations(entityCpa, audiences) { | |
| var operations = []; | |
| audiences.forEach(function(audience) { | |
| var stats = audience.getStatsFor(DATE_RANGE); | |
| var conversions = stats.getConversions(); | |
| if (conversions > 0) { | |
| var audienceCpa = stats.getCost() / stats.getConversions(); | |
| entityCpa = parseFloat(entityCpa); | |
| var modifier = (entityCpa / audienceCpa); | |
| var operation = {}; | |
| operation.audience = audience; | |
| operation.modifier = modifier; | |
| operations.push(operation); | |
| } | |
| }); | |
| return operations; | |
| } | |
| function campaignHasAnyCampaignLevelAudiences(campaign) { | |
| var totalNumEntities = campaign | |
| .targeting() | |
| .audiences() | |
| .get() | |
| .totalNumEntities(); | |
| return totalNumEntities > 0; | |
| } | |
| function applyBids(operations) { | |
| operations.forEach(function(operation) { | |
| operation.audience.bidding().setBidModifier(operation.modifier); | |
| }); | |
| } |
Hi,
the script is no longer working, as the URL for in-market audience list is different, the file is now .tsv and not .csv. But, even if converted and changed the URL - it returns this error: 'The audience tsv does not have the expected headers' .
I dont have the old original CSV anywhere, so - how to change the Headers so the script recognizes them properly?
I got it - thanks anyway!
Great script!
Hi kalaidzhiev,
I am facing the same issue in adwords script. Could you please share the solution
Hi Nivash, first hide your email ;) Second - i pre formatted .tsv to .csv - headers are like this: "Criterion ID, Category", then uploaded it to my own server and pointed that URL in the script. Hope I help - Cheers!
Hi I just discovered this script and would like to use it. I am running into the same problems above. Can you share what you changed the .csv link to? Can I upload as a google sheet and have it pull from there or would that completely change the way the script is written?
Thanks,
Hi @NivashBk,
the given solution with parsing the Csv gives a syntax error. A possible other way to parse tsv to the csv is using the following line of code:
var csv = Utilities.parseCsv(UrlFetchApp.fetch(downloadCsvUrl), '\t');
Hi,
I can't get the script working with Shopping Campaigns when audiences are set on campain level. It returns zero campaign level audiences. It is working fine when audiences are set at ad-group level!
Hi! Can someone help me please? I got this error and I have no idea how to fix it
| 04/10/2022 14:26:17 | Getting audience mapping |
|---|---|
| 04/10/2022 14:26:17 | Exception: Request failed for https://developers.google.com returned code 404. Truncated server response: <!doctype html> <meta name="google-signin-client-id" content="721724668570-nbkv1cfusk7kk4eni4p... (use muteHttpExceptions option to examine full response) at getInMarketAudienceMapping (Code:61:17) at main (Code:40:5) |
Hi,
the script is no longer working, as the URL for in-market audience list is different, the file is now .tsv and not .csv. But, even if converted and changed the URL - it returns this error: 'The audience tsv does not have the expected headers' .
I dont have the old original CSV anywhere, so - how to change the Headers so the script recognizes them properly?