Last active
June 18, 2019 17:18
-
-
Save kazdegroot/658aff45f46642d3b6b7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//------------------------------------------------ | |
// Auto add negative keywords to dynamic campaign | |
// Created by: Remko van der Zwaag & PDDS | |
// remkovanderzwaag.nl & pdds.nl | |
// More info: http://goo.gl/64PrMT | |
// 26-05-2015: Added Hybrid functionality | |
// 17-09-2015: Switched to negative keyword lists, | |
// among various other changes | |
// 16-03-2016: Inclusive version (eg. specify | |
// campaigns, instead of excluding) | |
// 05-04-2016: Support for co-existence with 'all' | |
//------------------------------------------------ | |
// This is a hybrid script, and will work on both account and MCC level | |
var spreadsheetId = '171OUhavrxfEv7xZg4jFr5ywQSZUKo_IS4nwM3m14aoU';// Google Spreadsheet with account info | |
// Can be shared with the 'all' CCKE sheet | |
// if using our standard sheet and fieldmapping | |
var prefillSpreadsheet = false; // When set to true, gets all accounts from the MCC account | |
// and automagically adds their name and id to the spreadsheet | |
// Use once, doesn't check for existing records | |
// switch back to false after use | |
// PREFERABLY RUN USING PREVIEW (true), CHANGE TO false AND SAVE | |
// The label on the target campaign(s) | |
// * Default - overwritable from spreadsheet | |
var label = 'RvdZ - Script: CCKE - target campaign'; | |
// The label on the source campaign(s) | |
// * Default - overwritable from spreadsheet | |
var campaign_label = 'RvdZ - Script: CCKE - source campaign'; | |
// The prefix of the negative keyword lists | |
// These lists will be created if necessary, and with the exception of adding them | |
// to the campaign, are the only objects to be modified. | |
// Because of the maximum of 5000 keywords per list, we make up to max_list_num lists, | |
// and fill them to the brim with negative keywords. | |
// NOTE: If there are more lists with this prefix, this script will still use them! | |
// * Default - overwritable from spreadsheet | |
var negative_list = 'RvdZ - Script: CCKE'; | |
var max_list_num = 10; | |
// Should we add the negative keyword with the same match type? | |
// Otherwise adds as EXACT | |
var keep_type = true; | |
// Clear the negative keyword lists before we fill them. If you switch this off, weird things might happen. | |
// WARNING: Will clear all negative keyword lists that start with the prefix in negative_list above!! | |
var clear_list = true; | |
// Special cases etc. | |
// DO NOT TOUCH below this line unless you know what you're doing. | |
// These are set to safe defaults that work for most cases. | |
// Minimum impressions needed for a keyword to be added to a list. Especially handy for large accounts | |
// because we can only a 5000 keywords per lists | |
var MIN_IMPRESSIONS = 1; | |
// Some facts about the world | |
// please don't touch unless google changes this | |
var MAX_NEG_LISTS = 20; | |
var MAX_ITEMS_PER_LIST = 5000; | |
// Mapping from spreadsheet rows to script settings. | |
// Empty spreadsheet fields are filled with the defaults | |
// If skip is set, and true, the script will skip that row | |
// (Used here for interaction with the 'all' CCKE script) | |
// You can modify this function to make this script work | |
// with a custom formatted spreadsheet. | |
function mapRowToInfo(row) { | |
return { | |
custId: row[1].trim(), | |
cust: row[0], | |
include_label: row[2], | |
target_label: row[3], | |
list_prefix: row[4], | |
list_amount: row[5], | |
skip: (row[6] == 'Yes') | |
}; | |
} | |
// DON'T TOUCH BELOW THIS LINE | |
function main() { | |
try { | |
// Uses parallel execution. Is limited to 50 accounts by Google. | |
if (prefillSpreadsheet) { | |
MccApp.accounts() | |
.withLimit(50) | |
.executeInParallel("getSSAccountInfo","saveSSAccountInfo"); | |
} else { | |
var ids = getSpreadsheetIds(); | |
if (ids.length > 0) { | |
MccApp.accounts() | |
.withIds(ids) | |
.withLimit(50) | |
.executeInParallel("processAccount"); | |
} | |
} | |
} catch (e) { | |
processAccount(); | |
} | |
beacon(); | |
} | |
// Get account name and id | |
function getSSAccountInfo() { | |
var result = { | |
custId: AdWordsApp.currentAccount().getCustomerId(), | |
cust: AdWordsApp.currentAccount().getName() | |
}; | |
Logger.log(result); | |
return JSON.stringify(result); | |
} | |
// Save account info to the spreadsheet | |
function saveSSAccountInfo(response) { | |
var ss; | |
try { | |
ss = SpreadsheetApp.openById(spreadsheetId); | |
} catch (e) { | |
} | |
ss = ss.getSheets()[0]; | |
ss.appendRow(["Account Name", "Account ID", "Source Label", "Target Label", "List Prefix", "List Amount", "Run Auto"]); | |
for (var i in response) { | |
if(!response[i].getReturnValue()) { continue; } | |
var rep = JSON.parse(response[i].getReturnValue()); | |
Logger.log(rep); | |
ss.appendRow([rep.cust, rep.custId]); | |
} | |
} | |
function getSpreadsheetIds() { | |
var ids = [], | |
ss, | |
reAWId = /^([0-9]{3})-([0-9]{3})-([0-9]{4})$/; | |
try { | |
ss = SpreadsheetApp.openById(spreadsheetId); | |
} catch (e) { | |
return ids; | |
} | |
ss = ss.getSheets()[0]; | |
var rows = parseInt(ss.getLastRow()); | |
var range = ss.getRange("A1:Z" + rows).getValues(); | |
for (var i = 0; i < rows; i++) { | |
var account = mapRowToInfo(range[i]); | |
if (!reAWId.test(account.custId) || account.skip) { | |
continue; | |
} | |
ids.push(account.custId); | |
} | |
return ids; | |
} | |
// Fetch info for current account from the spreadsheet | |
// MCC scripts don't seem to support shared state between | |
// Parallel executions, so we need to do this fresh for every account | |
// Uses default info from 'defaults' set in script, and replaces with | |
// values from spreadsheet where possible | |
function getAccountInfo() { | |
var ss; | |
var reAWId = /^([0-9]{3})-([0-9]{3})-([0-9]{4})$/; | |
var protoAccount = { | |
custId: AdWordsApp.currentAccount().getCustomerId(), | |
cust: AdWordsApp.currentAccount().getName(), | |
include_label: campaign_label, | |
target_label: label, | |
list_prefix: negative_list, | |
list_amount: max_list_num | |
}; | |
try { | |
ss = SpreadsheetApp.openById(spreadsheetId); | |
} catch (e) { | |
return protoAccount; | |
} | |
ss = ss.getSheets()[0]; | |
var rows = parseInt(ss.getLastRow()); | |
var range = ss.getRange("A1:Z" + rows).getValues(); | |
var ret_account; | |
for (var i = 0; i < rows; i++) { | |
var account = mapRowToInfo(range[i]); | |
if (account.skip) { | |
continue; | |
} | |
if (!reAWId.test(account.custId) || account.custId !== protoAccount.custId) { | |
continue; | |
} | |
for(var key in account) { | |
if (account[key] === '' || account[key] === '_ALL_') { | |
account[key] = protoAccount[key]; | |
} | |
} | |
return account; | |
} | |
return protoAccount; | |
} | |
function processAccount() { | |
var account = getAccountInfo(); | |
Logger.log(account); | |
try { | |
var campaigns = getCampaignsWithLabelArray(account.target_label); | |
} catch (ex) { | |
return; | |
} | |
if (campaigns.length === 0) { | |
return; | |
} | |
var include_list = []; | |
if (account.include_label !== '') { | |
try { | |
var iterator = getCampaignsWithLabel(account.include_label); | |
while (iterator.hasNext()) { | |
include_list.push(iterator.next().getId()); | |
} | |
} catch (ex) { | |
// pass | |
} | |
} | |
var keywords = getAllKeywords(include_list); | |
var numLists = Math.min(account.list_amount, Math.ceil(keywords.length/MAX_ITEMS_PER_LIST)); | |
var negativeKeywordLists = getNegativeKeywordLists(account.list_prefix, clear_list, numLists); | |
addKeywordsToLists(keywords, negativeKeywordLists); | |
addListsToCampaign(negativeKeywordLists, campaigns); | |
} | |
function addKeywordsToLists(keywords, lists) { | |
var numLists = Math.min(lists.length, Math.ceil(keywords.length/MAX_ITEMS_PER_LIST)); | |
for (var i = 0; i < numLists; i++) { | |
var keywordSlice = keywords.slice(i * MAX_ITEMS_PER_LIST, (i + 1) * MAX_ITEMS_PER_LIST); | |
lists[i].addNegativeKeywords(keywordSlice); | |
} | |
} | |
function addListsToCampaign(lists, campaigns) { | |
for (var j in campaigns) { | |
for (var i in lists) { | |
campaigns[j].addNegativeKeywordList(lists[i]); | |
} | |
} | |
} | |
function getNegativeKeywordLists(prefix, clear, minimum) { | |
if (minimum == undefined) { | |
minimum = 1; | |
} else if (minimum > MAX_NEG_LISTS) { | |
minimum = MAX_NEG_LISTS; | |
} | |
var lists = []; | |
var negativeKeywordListIterator = | |
AdWordsApp.negativeKeywordLists() | |
.withCondition("Name STARTS_WITH '" + prefix + " - '") | |
.get(); | |
if (negativeKeywordListIterator.totalNumEntities() != 0) { | |
while (negativeKeywordListIterator.hasNext()) { | |
var negativeKeywordList = negativeKeywordListIterator.next(); | |
if (clear) { | |
clearNegativeKeywordList(negativeKeywordList); | |
} | |
lists.push(negativeKeywordList); | |
} | |
} | |
while (lists.length < minimum) { | |
var builder = AdWordsApp.newNegativeKeywordListBuilder() | |
.withName(prefix + ' - ' + (lists.length + 1)) | |
.build(); | |
lists.push(builder.getResult()); | |
} | |
return lists; | |
} | |
function clearNegativeKeywordList(negativeKeywordList) { | |
var sharedNegativeKeywordIterator = | |
negativeKeywordList.negativeKeywords().get(); | |
while (sharedNegativeKeywordIterator.hasNext()) { | |
sharedNegativeKeywordIterator.next().remove(); | |
} | |
} | |
function getAllKeywords(campaignIds) { | |
return getAllKeywordsReport(campaignIds).slice(0,MAX_NEG_LISTS * MAX_ITEMS_PER_LIST); | |
} | |
// Use reports: moar speed | |
function getAllKeywordsReport(campaignIds) { | |
var result = {}; | |
var options = { includeZeroImpressions : true }; // Include keywords that aren't used | |
// AWQL query to find all keywords in the account | |
var query = "SELECT Criteria, KeywordMatchType " + | |
"FROM KEYWORDS_PERFORMANCE_REPORT " + | |
"WHERE " + | |
" Impressions >= " + MIN_IMPRESSIONS + " AND " + | |
" IsNegative = FALSE AND " + | |
" Status = ENABLED AND " + | |
" CampaignStatus = ENABLED AND " + | |
" AdGroupStatus = ENABLED AND " + | |
" AdNetworkType1 != 'CONTENT' AND " + | |
" AdNetworkType2 != 'CONTENT'"; | |
if (campaignIds.length > 0) { | |
query += " AND CampaignId IN [" + campaignIds.join(", ") + "]"; | |
} | |
query += " DURING LAST_7_DAYS"; | |
Logger.log(query); | |
var keywords = AdWordsApp.report(query, options).rows(); | |
var i = 0; | |
while (keywords.hasNext()) { | |
i++; | |
var keyword = keywords.next(); | |
keyword = resetKeyword(keyword.Criteria, keyword.KeywordMatchType); | |
result[keyword] = true; | |
// Check every 10000 rows if the result is going to be over the maxiumum number of keywords we can use | |
if (i % 10000 == 0) { | |
if (Object.keys(result).length > MAX_NEG_LISTS * MAX_ITEMS_PER_LIST ) { | |
break; | |
} | |
} | |
} | |
result = Object.keys(result); | |
return result; | |
} | |
function resetKeyword(keyword, type) { | |
keyword = keyword.replace(/\+/g, ''); | |
if (!keep_type) { | |
if (keyword.substr(0,1) == '"' || keyword.substr(0,1) == '[') { | |
keyword = keyword.substr(1,-1); | |
} | |
keyword = '[' + keyword + ']'; | |
} else if (type) { | |
if (type == 'Exact') { | |
keyword = '[' + keyword + ']'; | |
} else if (type == 'Phrase') { | |
keyword = '"' + keyword + '"'; | |
} | |
} | |
return keyword; | |
} | |
function getCampaignWithLabel(label) { | |
var campaignIterator = getCampaignsWithLabel(label); | |
if (campaignIterator.hasNext()) { | |
return campaignIterator.next(); | |
} | |
return null; | |
} | |
function getCampaignsWithLabel(label) { | |
return AdWordsApp.campaigns() | |
.withCondition("LabelNames CONTAINS_ANY ['" + label + "']") | |
.get(); | |
} | |
function getCampaignsWithLabelArray(label) { | |
var iterator = getCampaignsWithLabel(label); | |
var campaigns = []; | |
while (iterator.hasNext()) { | |
campaigns.push(iterator.next()); | |
} | |
return campaigns; | |
} | |
/******************************** | |
* Track Script Runs in Google Analytics | |
* Created By: Russ Savage | |
* FreeAdWordsScripts.com | |
********************************/ | |
function beacon() { | |
var TAG_ID = 'UA-31686665-7'; | |
var CAMPAIGN_SOURCE = 'adwords'; | |
var CAMPAIGN_MEDIUM = 'scripts'; | |
var CAMPAIGN_NAME = 'script - negative keywords for DSA'; | |
var HOSTNAME = 'remkovanderzwaag.nl'; | |
var PAGE = '/scripts/dsa-negatives'; | |
var DOMAIN_LINK = 'http://'+HOSTNAME+PAGE; | |
//Pulled from: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript | |
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, | |
function(c) {var r = Math.random()*16|0,v=c=='x'?r:r&0x3|0x8;return v.toString(16);}); | |
var url = 'http://www.google-analytics.com/collect?'; | |
var payload = { | |
'v':1,'tid':TAG_ID,'cid':uuid, | |
't':'pageview','cs':CAMPAIGN_SOURCE,'cm':CAMPAIGN_MEDIUM,'cn':CAMPAIGN_NAME, | |
'dl':DOMAIN_LINK | |
}; | |
var qs = ''; | |
for(var key in payload) { | |
qs += key + '=' + encodeURIComponent(payload[key]) + '&'; | |
} | |
url += qs.substring(0,qs.length-1); | |
UrlFetchApp.fetch(url); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment