-
-
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?