Thanks for the clarification, I have updated the sections for processResults() and processAccount(). Please take a look at my updated code so far and if possible let me know where to paste the rest of the NKC code / functions. Should I remove the processIntermediateResults(results) and initializeExecution(customerIds) since I'm not replacing the code with my own?
// 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
//
//
// 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 Large Manager Hierarchy Template
*
* @overview The Large Manager Hierarchy Template script provides a general way
* to run script logic on all client accounts within a manager account
* hierarchy, splitting the work across multiple executions if necessary.
* Each execution of the script processes a subset of the hierarchy's client
* accounts that it hadn't previously processed, saving the results to a
* temporary file on Drive. Once the script processes the final subset of
* accounts, the consolidated results can be output and the cycle can begin
* again.
* See
* for more details.
*
*
* @version 1.0
*
* @changelog
* - version 1.0
* - Released initial version.
*/
/*************** START OF YOUR IMPLEMENTATION ***************/
var TEMPLATE_CONFIG = {
// The name of the file that will be created on Drive to store data
// between executions of the script. You must use a different
// filename for each each script running in the account, or data
// from different scripts may overwrite one another.
FILENAME: 'UNIQUE_FILENAME_HERE',
// The minimum number of days between the start of each cycle.
MIN_FREQUENCY: 1,
// Controls whether child accounts will be processed in parallel (true)
// or sequentially (false).
USE_PARALLEL_MODE: true,
// Controls the maximum number of accounts that will be processed in a
// single script execution.
MAX_ACCOUNTS: 50,
// A list of ManagedAccountSelector conditions to restrict the population
// of child accounts that will be processed. Leave blank or comment out
// to include all child accounts.
ACCOUNT_CONDITIONS: []
};
// The possible statuses for the script as a whole or an individual account.
var Statuses = {
NOT_STARTED: 'Not Started',
STARTED: 'Started',
FAILED: 'Failed',
COMPLETE: 'Complete'
};
/**
* Your main logic for initializing a cycle for your script.
*
* @param {Array.<string>} customerIds The customerIds that this cycle
* will process.
*/
function initializeCycle(customerIds) {
// REPLACE WITH YOUR IMPLEMENTATION
// This example simply prints the accounts that will be process in
// this cycle.
Logger.log('Accounts to be processed this cycle:');
for (var i = 0; i < customerIds.length; i++) {
Logger.log(customerIds[i]);
}
}
/**
* Your main logic for initializing a single execution of the script.
*
* @param {Array.<string>} customerIds The customerIds that this
* execution will process.
*/
function initializeExecution(customerIds) {
// REPLACE WITH YOUR IMPLEMENTATION
// This example simply prints the accounts that will be process in
// this execution.
Logger.log('Accounts to be processed this execution:');
for (var i = 0; i < customerIds.length; i++) {
Logger.log(customerIds[i]);
}
}
/**
* Your main logic for processing a single Google Ads account. This function
* can perform any sort of processing on the account, followed by
* outputting results immediately (e.g., sending an email, saving to a
* spreadsheet, etc.) and/or returning results to be output later, e.g.,
* to be combined with the output from other accounts.
*
* @return {Object} An object containing any results of your processing
* that you want to output later.
*/
/**
* Finds conflicts and returns the results in a format suitable for
* executeInParallel().
*
* @return {string} JSON stringified list of conflict objects.
*/
function processAccount() {
return JSON.stringify(findAllConflicts());
}
/**
* Your main logic for consolidating or outputting results after
* a single execution of the script. These single execution results may
* reflect the processing on only a subset of your accounts.
*
* @param {Object.<string, {
* status: string,
* returnValue: Object,
* error: string
* }>} results The results for the accounts processed in this
* execution of the script, keyed by customerId. The status will be
* Statuses.COMPLETE if the account was processed successfully,
* Statuses.FAILED if there was an error, and Statuses.STARTED if it
* timed out. The returnValue field is present when the status is
* Statuses.COMPLETE and corresponds to the object you returned in
* processAccount(). The error field is present when the status is
* Statuses.FAILED.
*/
function processIntermediateResults(results) {
// REPLACE WITH YOUR IMPLEMENTATION
// This example simply logs the number of campaigns and ad groups
// in each of the accounts successfully processed in this execution.
Logger.log('Results of this execution:');
for (var customerId in results) {
var result = results[customerId];
if (result.status == Statuses.COMPLETE) {
Logger.log(customerId + ': ' + result.returnValue.numCampaigns +
' campaigns, ' + result.returnValue.numAdGroups +
' ad groups');
} else if (result.status == Statuses.STARTED) {
Logger.log(customerId + ': timed out');
} else {
Logger.log(customerId + ': failed due to "' + result.error + '"');
}
}
}
/**
* Your main logic for consolidating or outputting results after
* the script has executed a complete cycle across all of your accounts.
* This function will only be called once per complete cycle.
*
* @param {Object.<string, {
* status: string,
* returnValue: Object,
* error: string
* }>} results The results for the accounts processed in this
* execution of the script, keyed by customerId. The status will be
* Statuses.COMPLETE if the account was processed successfully,
* Statuses.FAILED if there was an error, and Statuses.STARTED if it
* timed out. The returnValue field is present when the status is
* Statuses.COMPLETE and corresponds to the object you returned in
* processAccount(). The error field is present when the status is
* Statuses.FAILED.
*/
/**
* Outputs conflicts to a spreadsheet and sends an email alert if applicable.
*
* @param {Array.<Object>} results A list of ExecutionResult objects.
*/
function processFinalResults(results) {
var hasConflicts = false;
var spreadsheet = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
if (CONFIG.COPY_SPREADSHEET) {
spreadsheet = spreadsheet.copy('Negative Keyword Conflicts');
}
initializeSpreadsheet(spreadsheet);
for (var i = 0; i < results.length; i++) {
if (!results[i].getError()) {
hasConflicts = outputConflicts(spreadsheet, results[i].getCustomerId(),
JSON.parse(results[i].getReturnValue())) || hasConflicts;
} else {
Logger.log('Processing for ' + results[i].getCustomerId() + ' failed.');
}
}
if (hasConflicts && CONFIG.RECIPIENT_EMAILS) {
sendEmail(spreadsheet);
}
}
/**************** END OF YOUR IMPLEMENTATION ****************/
/**************** START OF STANDARD TEMPLATE ****************/
// Whether or not the script is running in a manager account.
var IS_MANAGER = typeof AdsManagerApp !== 'undefined';
// The maximum number of accounts that can be processed when using
// executeInParallel().
var MAX_PARALLEL = 50;
// The possible modes in which the script can execute.
var Modes = {
SINGLE: 'Single',
MANAGER_SEQUENTIAL: 'Manager Sequential',
MANAGER_PARALLEL: 'Manager Parallel'
};
function main() {
var mode = getMode();
stateManager.loadState();
// The last execution may have attempted the final set of accounts but
// failed to actually complete the cycle because of a timeout in
// processIntermediateResults(). In that case, complete the cycle now.
if (stateManager.getAccountsWithStatus().length > 0) {
completeCycleIfNecessary();
}
// If the cycle is complete and enough time has passed since the start of
// the last cycle, reset it to begin a new cycle.
if (stateManager.getStatus() == Statuses.COMPLETE) {
if (dayDifference(stateManager.getLastStartTime(), new Date()) >
TEMPLATE_CONFIG.MIN_FREQUENCY) {
stateManager.resetState();
} else {
Logger.log('Waiting until ' + TEMPLATE_CONFIG.MIN_FREQUENCY +
' days have elapsed since the start of the last cycle.');
return;
}
}
// Find accounts that have not yet been processed. If this is the
// beginning of a new cycle, this will be all accounts.
var customerIds =
stateManager.getAccountsWithStatus(Statuses.NOT_STARTED);
// The status will be Statuses.NOT_STARTED if this is the very first
// execution or if the cycle was just reset. In either case, it is the
// beginning of a new cycle.
if (stateManager.getStatus() == Statuses.NOT_STARTED) {
stateManager.setStatus(Statuses.STARTED);
stateManager.saveState();
initializeCycle(customerIds);
}
// Don't attempt to process more accounts than specified, and
// enforce the limit on parallel execution if necessary.
var accountLimit = TEMPLATE_CONFIG.MAX_ACCOUNTS;
if (mode == Modes.MANAGER_PARALLEL) {
accountLimit = Math.min(MAX_PARALLEL, accountLimit);
}
var customerIdsToProcess = customerIds.slice(0, accountLimit);
// Save state so that we can detect when an account timed out by it still
// being in the STARTED state.
stateManager.setAccountsWithStatus(customerIdsToProcess, Statuses.STARTED);
stateManager.saveState();
initializeExecution(customerIdsToProcess);
executeByMode(mode, customerIdsToProcess);
}
/**
* Runs the script on a list of accounts in a given mode.
*
* @param {string} mode The mode the script should run in.
* @param {Array.<string>} customerIds The customerIds that this execution
* should process. If mode is Modes.SINGLE, customerIds must contain
* a single element which is the customerId of the Google Ads account.
*/
function executeByMode(mode, customerIds) {
switch (mode) {
case Modes.SINGLE:
var results = {};
results[customerIds[0]] = tryProcessAccount();
completeExecution(results);
break;
case Modes.MANAGER_SEQUENTIAL:
var accounts = AdsManagerApp.accounts().withIds(customerIds).get();
var results = {};
var managerAccount = AdsApp.currentAccount();
while (accounts.hasNext()) {
var account = accounts.next();
AdsManagerApp.select(account);
results[account.getCustomerId()] = tryProcessAccount();
}
AdsManagerApp.select(managerAccount);
completeExecution(results);
break;
case Modes.MANAGER_PARALLEL:
if (customerIds.length == 0) {
completeExecution({});
} else {
var accountSelector = AdsManagerApp.accounts().withIds(customerIds);
accountSelector.executeInParallel('parallelFunction',
'parallelCallback');
}
break;
}
}
/**
* Attempts to process the current Google Ads account.
*
* @return {Object} The result of the processing if successful, or
* an object with status Statuses.FAILED and the error message
* if unsuccessful.
*/
function tryProcessAccount() {
try {
return {
status: Statuses.COMPLETE,
returnValue: processAccount()
};
} catch (e) {
return {
status: Statuses.FAILED,
error: e.message
};
}
}
/**
* The function given to executeInParallel() when running in parallel mode.
* This helper function is necessary so that the return value of
* processAccount() is transformed into a string as required by
* executeInParallel().
*
* @return {string} JSON string representing the return value of
* processAccount().
*/
function parallelFunction() {
var returnValue = processAccount();
return JSON.stringify(returnValue);
}
/**
* The callback given to executeInParallel() when running in parallel mode.
* Processes the execution results into the format used by all execution
* modes.
*
* @param {Array.<Object>} executionResults An array of execution results
* from a parallel execution.
*/
function parallelCallback(executionResults) {
var results = {};
for (var i = 0; i < executionResults.length; i++) {
var executionResult = executionResults[i];
var status;
if (executionResult.getStatus() == 'OK') {
status = Statuses.COMPLETE;
} else if (executionResult.getStatus() == 'TIMEOUT') {
status = Statuses.STARTED;
} else {
status = Statuses.FAILED;
}
results[executionResult.getCustomerId()] = {
status: status,
returnValue: JSON.parse(executionResult.getReturnValue()),
error: executionResult.getError()
};
}
// After executeInParallel(), variables in global scope are reevaluated,
// so reload the state.
stateManager.loadState();
completeExecution(results);
}
/**
* Completes a single execution of the script by saving the results and
* calling the intermediate and final result handlers as necessary.
*
* @param {Object.<string, {
* status: string,
* returnValue: Object,
* error: string
* }>} results The results of the current execution of the script.
*/
function completeExecution(results) {
for (var customerId in results) {
var result = results[customerId];
stateManager.setAccountWithResult(customerId, result);
}
stateManager.saveState();
processIntermediateResults(results);
completeCycleIfNecessary();
}
/**
* Completes a full cycle of the script if all accounts have been attempted
* but the cycle has not been marked as complete yet.
*/
function completeCycleIfNecessary() {
if (stateManager.getAccountsWithStatus(Statuses.NOT_STARTED).length == 0 &&
stateManager.getStatus() != Statuses.COMPLETE) {
stateManager.setStatus(Statuses.COMPLETE);
stateManager.saveState();
processFinalResults(stateManager.getResults());
}
}
/**
* Determines what mode the script should run in.
*
* @return {string} The mode to run in.
*/
function getMode() {
if (IS_MANAGER) {
if (TEMPLATE_CONFIG.USE_PARALLEL_MODE) {
return Modes.MANAGER_PARALLEL;
} else {
return Modes.MANAGER_SEQUENTIAL;
}
} else {
return Modes.SINGLE;
}
}
/**
* Finds all customer IDs that the script could process. For a single account,
* this is simply the account itself.
*
* @return {Array.<string>} A list of customer IDs.
*/
function getCustomerIdsPopulation() {
if (IS_MANAGER) {
var customerIds = [];
var selector = AdsManagerApp.accounts();
var conditions = TEMPLATE_CONFIG.ACCOUNT_CONDITIONS || [];
for (var i = 0; i < conditions.length; i++) {
selector = selector.withCondition(conditions[i]);
}
var accounts = selector.get();
while (accounts.hasNext()) {
customerIds.push(accounts.next().getCustomerId());
}
return customerIds;
} else {
return [AdsApp.currentAccount().getCustomerId()];
}
}
/**
* Returns the number of days between two dates.
*
* @param {Object} from The older Date object.
* @param {Object} to The newer (more recent) Date object.
* @return {number} The number of days between the given dates (possibly
* fractional).
*/
function dayDifference(from, to) {
return (to.getTime() - from.getTime()) / (24 * 3600 * 1000);
}
/**
* Loads a JavaScript object previously saved as JSON to a file on Drive.
*
* @param {string} filename The name of the file in the account's root Drive
* folder where the object was previously saved.
* @return {Object} The JavaScript object, or null if the file was not found.
*/
function loadObject(filename) {
var files = DriveApp.getRootFolder().getFilesByName(filename);
if (!files.hasNext()) {
return null;
} else {
var file = files.next();
if (files.hasNext()) {
throwDuplicateFileException(filename);
}
return JSON.parse(file.getBlob().getDataAsString());
}
}
/**
* Saves a JavaScript object as JSON to a file on Drive. An existing file with
* the same name is overwritten.
*
* @param {string} filename The name of the file in the account's root Drive
* folder where the object should be saved.
* @param {obj} obj The object to save.
*/
function saveObject(filename, obj) {
var files = DriveApp.getRootFolder().getFilesByName(filename);
if (!files.hasNext()) {
DriveApp.createFile(filename, JSON.stringify(obj));
} else {
var file = files.next();
if (files.hasNext()) {
throwDuplicateFileException(filename);
}
file.setContent(JSON.stringify(obj));
}
}
/**
* Throws an exception if there are multiple files with the same name.
*
* @param {string} filename The filename that caused the error.
*/
function throwDuplicateFileException(filename) {
throw 'Multiple files named ' + filename + ' detected. Please ensure ' +
'there is only one file named ' + filename + ' and try again.';
}
var stateManager = (function() {
/**
* @type {{ * cycle: {
* status: string,
* lastUpdate: string,
* startTime: string
* },
* accounts: Object.<string, {
* status: string,
* lastUpdate: string,
* returnValue: Object
* }>
* }} */
var state;
/**
* Loads the saved state of the script. If there is no previously
* saved state, sets the state to an initial default.
*/
var loadState = function() {
state = loadObject(TEMPLATE_CONFIG.FILENAME);
if (!state) {
resetState();
}
};
/**
* Saves the state of the script to Drive.
*/
var saveState = function() {
saveObject(TEMPLATE_CONFIG.FILENAME, state);
};
/**
* Resets the state to an initial default.
*/
var resetState = function() {
state = {};
var date = Date();
state.cycle = {
status: Statuses.NOT_STARTED,
lastUpdate: date,
startTime: date
};
state.accounts = {};
var customerIds = getCustomerIdsPopulation();
for (var i = 0; i < customerIds.length; i++) {
state.accounts[customerIds[i]] = {
status: Statuses.NOT_STARTED,
lastUpdate: date
};
}
};
/**
* Gets the status of the current cycle.
*
* @return {string} The status of the current cycle.
*/
var getStatus = function() {
return state.cycle.status;
};
/**
* Sets the status of the current cycle.
*
* @param {string} status The status of the current cycle.
*/
var setStatus = function(status) {
var date = Date();
if (status == Statuses.IN_PROGRESS &&
state.cycle.status == Statuses.NOT_STARTED) {
state.cycle.startTime = date;
}
state.cycle.status = status;
state.cycle.lastUpdate = date;
};
/**
* Gets the start time of the current cycle.
*
* @return {Object} Date object for the start of the last cycle.
*/
var getLastStartTime = function() {
return new Date(state.cycle.startTime);
};
/**
* Gets accounts in the current cycle with a particular status.
*
* @param {string} status The status of the accounts to get.
* If null, all accounts are retrieved.
* @return {Array.<string>} A list of matching customerIds.
*/
var getAccountsWithStatus = function(status) {
var customerIds = [];
for (var customerId in state.accounts) {
if (!status || state.accounts[customerId].status == status) {
customerIds.push(customerId);
}
}
return customerIds;
};
/**
* Sets accounts in the current cycle with a particular status.
*
* @param {Array.<string>} customerIds A list of customerIds.
* @param {string} status A status to apply to those customerIds.
*/
var setAccountsWithStatus = function(customerIds, status) {
var date = Date();
for (var i = 0; i < customerIds.length; i++) {
var customerId = customerIds[i];
if (state.accounts[customerId]) {
state.accounts[customerId].status = status;
state.accounts[customerId].lastUpdate = date;
}
}
};
/**
* Registers the processing of a particular account with a result.
*
* @param {string} customerId The account that was processed.
* @param {{ * status: string,
* returnValue: Object
* error: string
* }} result The object to save for that account.
*/
var setAccountWithResult = function(customerId, result) {
if (state.accounts[customerId]) {
state.accounts[customerId].status = result.status;
state.accounts[customerId].returnValue = result.returnValue;
state.accounts[customerId].error = result.error;
state.accounts[customerId].lastUpdate = Date();
}
};
/**
* Gets the current results of the cycle for all accounts.
*
* @return {Object.<string, {
* status: string,
* lastUpdate: string,
* returnValue: Object,
* error: string
* }>} The results processed by the script during the cycle,
* keyed by account.
*/
var getResults = function() {
return state.accounts;
};
return {
loadState: loadState,
saveState: saveState,
resetState: resetState,
getStatus: getStatus,
setStatus: setStatus,
getLastStartTime: getLastStartTime,
getAccountsWithStatus: getAccountsWithStatus,
setAccountsWithStatus: setAccountsWithStatus,
setAccountWithResult: setAccountWithResult,
getResults: getResults
};
})();
/***************** END OF STANDARD TEMPLATE *****************/
// 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
//
//
// 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 Large Manager Hierarchy Template
*
* @overview The Large Manager Hierarchy Template script provides a general way
* to run script logic on all client accounts within a manager account
* hierarchy, splitting the work across multiple executions if necessary.
* Each execution of the script processes a subset of the hierarchy's client
* accounts that it hadn't previously processed, saving the results to a
* temporary file on Drive. Once the script processes the final subset of
* accounts, the consolidated results can be output and the cycle can begin
* again.
* See
* for more details.
*
*
* @version 1.0
*
* @changelog
* - version 1.0
* - Released initial version.
*/
/*************** START OF YOUR IMPLEMENTATION ***************/
var TEMPLATE_CONFIG = {
// The name of the file that will be created on Drive to store data
// between executions of the script. You must use a different
// filename for each each script running in the account, or data
// from different scripts may overwrite one another.
FILENAME: 'UNIQUE_FILENAME_HERE',
// The minimum number of days between the start of each cycle.
MIN_FREQUENCY: 1,
// Controls whether child accounts will be processed in parallel (true)
// or sequentially (false).
USE_PARALLEL_MODE: true,
// Controls the maximum number of accounts that will be processed in a
// single script execution.
MAX_ACCOUNTS: 50,
// A list of ManagedAccountSelector conditions to restrict the population
// of child accounts that will be processed. Leave blank or comment out
// to include all child accounts.
ACCOUNT_CONDITIONS: []
};
// The possible statuses for the script as a whole or an individual account.
var Statuses = {
NOT_STARTED: 'Not Started',
STARTED: 'Started',
FAILED: 'Failed',
COMPLETE: 'Complete'
};
/**
* Your main logic for initializing a cycle for your script.
*
* @param {Array.<string>} customerIds The customerIds that this cycle
* will process.
*/
function initializeCycle(customerIds) {
// REPLACE WITH YOUR IMPLEMENTATION
// This example simply prints the accounts that will be process in
// this cycle.
Logger.log('Accounts to be processed this cycle:');
for (var i = 0; i < customerIds.length; i++) {
Logger.log(customerIds[i]);
}
}
/**
* Your main logic for initializing a single execution of the script.
*
* @param {Array.<string>} customerIds The customerIds that this
* execution will process.
*/
function initializeExecution(customerIds) {
// REPLACE WITH YOUR IMPLEMENTATION
// This example simply prints the accounts that will be process in
// this execution.
Logger.log('Accounts to be processed this execution:');
for (var i = 0; i < customerIds.length; i++) {
Logger.log(customerIds[i]);
}
}
/**
* Your main logic for processing a single Google Ads account. This function
* can perform any sort of processing on the account, followed by
* outputting results immediately (e.g., sending an email, saving to a
* spreadsheet, etc.) and/or returning results to be output later, e.g.,
* to be combined with the output from other accounts.
*
* @return {Object} An object containing any results of your processing
* that you want to output later.
*/
/**
* Finds conflicts and returns the results in a format suitable for
* executeInParallel().
*
* @return {string} JSON stringified list of conflict objects.
*/
function processAccount() {
return JSON.stringify(findAllConflicts());
}
/**
* Your main logic for consolidating or outputting results after
* a single execution of the script. These single execution results may
* reflect the processing on only a subset of your accounts.
*
* @param {Object.<string, {
* status: string,
* returnValue: Object,
* error: string
* }>} results The results for the accounts processed in this
* execution of the script, keyed by customerId. The status will be
* Statuses.COMPLETE if the account was processed successfully,
* Statuses.FAILED if there was an error, and Statuses.STARTED if it
* timed out. The returnValue field is present when the status is
* Statuses.COMPLETE and corresponds to the object you returned in
* processAccount(). The error field is present when the status is
* Statuses.FAILED.
*/
function processIntermediateResults(results) {
// REPLACE WITH YOUR IMPLEMENTATION
// This example simply logs the number of campaigns and ad groups
// in each of the accounts successfully processed in this execution.
Logger.log('Results of this execution:');
for (var customerId in results) {
var result = results[customerId];
if (result.status == Statuses.COMPLETE) {
Logger.log(customerId + ': ' + result.returnValue.numCampaigns +
' campaigns, ' + result.returnValue.numAdGroups +
' ad groups');
} else if (result.status == Statuses.STARTED) {
Logger.log(customerId + ': timed out');
} else {
Logger.log(customerId + ': failed due to "' + result.error + '"');
}
}
}
/**
* Your main logic for consolidating or outputting results after
* the script has executed a complete cycle across all of your accounts.
* This function will only be called once per complete cycle.
*
* @param {Object.<string, {
* status: string,
* returnValue: Object,
* error: string
* }>} results The results for the accounts processed in this
* execution of the script, keyed by customerId. The status will be
* Statuses.COMPLETE if the account was processed successfully,
* Statuses.FAILED if there was an error, and Statuses.STARTED if it
* timed out. The returnValue field is present when the status is
* Statuses.COMPLETE and corresponds to the object you returned in
* processAccount(). The error field is present when the status is
* Statuses.FAILED.
*/
/**
* Outputs conflicts to a spreadsheet and sends an email alert if applicable.
*
* @param {Array.<Object>} results A list of ExecutionResult objects.
*/
function processFinalResults(results) {
var hasConflicts = false;
var spreadsheet = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
if (CONFIG.COPY_SPREADSHEET) {
spreadsheet = spreadsheet.copy('Negative Keyword Conflicts');
}
initializeSpreadsheet(spreadsheet);
for (var i = 0; i < results.length; i++) {
if (!results[i].getError()) {
hasConflicts = outputConflicts(spreadsheet, results[i].getCustomerId(),
JSON.parse(results[i].getReturnValue())) || hasConflicts;
} else {
Logger.log('Processing for ' + results[i].getCustomerId() + ' failed.');
}
}
if (hasConflicts && CONFIG.RECIPIENT_EMAILS) {
sendEmail(spreadsheet);
}
}
/**************** END OF YOUR IMPLEMENTATION ****************/
/**************** START OF STANDARD TEMPLATE ****************/
// Whether or not the script is running in a manager account.
var IS_MANAGER = typeof AdsManagerApp !== 'undefined';
// The maximum number of accounts that can be processed when using
// executeInParallel().
var MAX_PARALLEL = 50;
// The possible modes in which the script can execute.
var Modes = {
SINGLE: 'Single',
MANAGER_SEQUENTIAL: 'Manager Sequential',
MANAGER_PARALLEL: 'Manager Parallel'
};
function main() {
var mode = getMode();
stateManager.loadState();
// The last execution may have attempted the final set of accounts but
// failed to actually complete the cycle because of a timeout in
// processIntermediateResults(). In that case, complete the cycle now.
if (stateManager.getAccountsWithStatus().length > 0) {
completeCycleIfNecessary();
}
// If the cycle is complete and enough time has passed since the start of
// the last cycle, reset it to begin a new cycle.
if (stateManager.getStatus() == Statuses.COMPLETE) {
if (dayDifference(stateManager.getLastStartTime(), new Date()) >
TEMPLATE_CONFIG.MIN_FREQUENCY) {
stateManager.resetState();
} else {
Logger.log('Waiting until ' + TEMPLATE_CONFIG.MIN_FREQUENCY +
' days have elapsed since the start of the last cycle.');
return;
}
}
// Find accounts that have not yet been processed. If this is the
// beginning of a new cycle, this will be all accounts.
var customerIds =
stateManager.getAccountsWithStatus(Statuses.NOT_STARTED);
// The status will be Statuses.NOT_STARTED if this is the very first
// execution or if the cycle was just reset. In either case, it is the
// beginning of a new cycle.
if (stateManager.getStatus() == Statuses.NOT_STARTED) {
stateManager.setStatus(Statuses.STARTED);
stateManager.saveState();
initializeCycle(customerIds);
}
// Don't attempt to process more accounts than specified, and
// enforce the limit on parallel execution if necessary.
var accountLimit = TEMPLATE_CONFIG.MAX_ACCOUNTS;
if (mode == Modes.MANAGER_PARALLEL) {
accountLimit = Math.min(MAX_PARALLEL, accountLimit);
}
var customerIdsToProcess = customerIds.slice(0, accountLimit);
// Save state so that we can detect when an account timed out by it still
// being in the STARTED state.
stateManager.setAccountsWithStatus(customerIdsToProcess, Statuses.STARTED);
stateManager.saveState();
initializeExecution(customerIdsToProcess);
executeByMode(mode, customerIdsToProcess);
}
/**
* Runs the script on a list of accounts in a given mode.
*
* @param {string} mode The mode the script should run in.
* @param {Array.<string>} customerIds The customerIds that this execution
* should process. If mode is Modes.SINGLE, customerIds must contain
* a single element which is the customerId of the Google Ads account.
*/
function executeByMode(mode, customerIds) {
switch (mode) {
case Modes.SINGLE:
var results = {};
results[customerIds[0]] = tryProcessAccount();
completeExecution(results);
break;
case Modes.MANAGER_SEQUENTIAL:
var accounts = AdsManagerApp.accounts().withIds(customerIds).get();
var results = {};
var managerAccount = AdsApp.currentAccount();
while (accounts.hasNext()) {
var account = accounts.next();
AdsManagerApp.select(account);
results[account.getCustomerId()] = tryProcessAccount();
}
AdsManagerApp.select(managerAccount);
completeExecution(results);
break;
case Modes.MANAGER_PARALLEL:
if (customerIds.length == 0) {
completeExecution({});
} else {
var accountSelector = AdsManagerApp.accounts().withIds(customerIds);
accountSelector.executeInParallel('parallelFunction',
'parallelCallback');
}
break;
}
}
/**
* Attempts to process the current Google Ads account.
*
* @return {Object} The result of the processing if successful, or
* an object with status Statuses.FAILED and the error message
* if unsuccessful.
*/
function tryProcessAccount() {
try {
return {
status: Statuses.COMPLETE,
returnValue: processAccount()
};
} catch (e) {
return {
status: Statuses.FAILED,
error: e.message
};
}
}
/**
* The function given to executeInParallel() when running in parallel mode.
* This helper function is necessary so that the return value of
* processAccount() is transformed into a string as required by
* executeInParallel().
*
* @return {string} JSON string representing the return value of
* processAccount().
*/
function parallelFunction() {
var returnValue = processAccount();
return JSON.stringify(returnValue);
}
/**
* The callback given to executeInParallel() when running in parallel mode.
* Processes the execution results into the format used by all execution
* modes.
*
* @param {Array.<Object>} executionResults An array of execution results
* from a parallel execution.
*/
function parallelCallback(executionResults) {
var results = {};
for (var i = 0; i < executionResults.length; i++) {
var executionResult = executionResults[i];
var status;
if (executionResult.getStatus() == 'OK') {
status = Statuses.COMPLETE;
} else if (executionResult.getStatus() == 'TIMEOUT') {
status = Statuses.STARTED;
} else {
status = Statuses.FAILED;
}
results[executionResult.getCustomerId()] = {
status: status,
returnValue: JSON.parse(executionResult.getReturnValue()),
error: executionResult.getError()
};
}
// After executeInParallel(), variables in global scope are reevaluated,
// so reload the state.
stateManager.loadState();
completeExecution(results);
}
/**
* Completes a single execution of the script by saving the results and
* calling the intermediate and final result handlers as necessary.
*
* @param {Object.<string, {
* status: string,
* returnValue: Object,
* error: string
* }>} results The results of the current execution of the script.
*/
function completeExecution(results) {
for (var customerId in results) {
var result = results[customerId];
stateManager.setAccountWithResult(customerId, result);
}
stateManager.saveState();
processIntermediateResults(results);
completeCycleIfNecessary();
}
/**
* Completes a full cycle of the script if all accounts have been attempted
* but the cycle has not been marked as complete yet.
*/
function completeCycleIfNecessary() {
if (stateManager.getAccountsWithStatus(Statuses.NOT_STARTED).length == 0 &&
stateManager.getStatus() != Statuses.COMPLETE) {
stateManager.setStatus(Statuses.COMPLETE);
stateManager.saveState();
processFinalResults(stateManager.getResults());
}
}
/**
* Determines what mode the script should run in.
*
* @return {string} The mode to run in.
*/
function getMode() {
if (IS_MANAGER) {
if (TEMPLATE_CONFIG.USE_PARALLEL_MODE) {
return Modes.MANAGER_PARALLEL;
} else {
return Modes.MANAGER_SEQUENTIAL;
}
} else {
return Modes.SINGLE;
}
}
/**
* Finds all customer IDs that the script could process. For a single account,
* this is simply the account itself.
*
* @return {Array.<string>} A list of customer IDs.
*/
function getCustomerIdsPopulation() {
if (IS_MANAGER) {
var customerIds = [];
var selector = AdsManagerApp.accounts();
var conditions = TEMPLATE_CONFIG.ACCOUNT_CONDITIONS || [];
for (var i = 0; i < conditions.length; i++) {
selector = selector.withCondition(conditions[i]);
}
var accounts = selector.get();
while (accounts.hasNext()) {
customerIds.push(accounts.next().getCustomerId());
}
return customerIds;
} else {
return [AdsApp.currentAccount().getCustomerId()];
}
}
/**
* Returns the number of days between two dates.
*
* @param {Object} from The older Date object.
* @param {Object} to The newer (more recent) Date object.
* @return {number} The number of days between the given dates (possibly
* fractional).
*/
function dayDifference(from, to) {
return (to.getTime() - from.getTime()) / (24 * 3600 * 1000);
}
/**
* Loads a JavaScript object previously saved as JSON to a file on Drive.
*
* @param {string} filename The name of the file in the account's root Drive
* folder where the object was previously saved.
* @return {Object} The JavaScript object, or null if the file was not found.
*/
function loadObject(filename) {
var files = DriveApp.getRootFolder().getFilesByName(filename);
if (!files.hasNext()) {
return null;
} else {
var file = files.next();
if (files.hasNext()) {
throwDuplicateFileException(filename);
}
return JSON.parse(file.getBlob().getDataAsString());
}
}
/**
* Saves a JavaScript object as JSON to a file on Drive. An existing file with
* the same name is overwritten.
*
* @param {string} filename The name of the file in the account's root Drive
* folder where the object should be saved.
* @param {obj} obj The object to save.
*/
function saveObject(filename, obj) {
var files = DriveApp.getRootFolder().getFilesByName(filename);
if (!files.hasNext()) {
DriveApp.createFile(filename, JSON.stringify(obj));
} else {
var file = files.next();
if (files.hasNext()) {
throwDuplicateFileException(filename);
}
file.setContent(JSON.stringify(obj));
}
}
/**
* Throws an exception if there are multiple files with the same name.
*
* @param {string} filename The filename that caused the error.
*/
function throwDuplicateFileException(filename) {
throw 'Multiple files named ' + filename + ' detected. Please ensure ' +
'there is only one file named ' + filename + ' and try again.';
}
var stateManager = (function() {
/**
* @type {{ * cycle: {
* status: string,
* lastUpdate: string,
* startTime: string
* },
* accounts: Object.<string, {
* status: string,
* lastUpdate: string,
* returnValue: Object
* }>
* }} */
var state;
/**
* Loads the saved state of the script. If there is no previously
* saved state, sets the state to an initial default.
*/
var loadState = function() {
state = loadObject(TEMPLATE_CONFIG.FILENAME);
if (!state) {
resetState();
}
};
/**
* Saves the state of the script to Drive.
*/
var saveState = function() {
saveObject(TEMPLATE_CONFIG.FILENAME, state);
};
/**
* Resets the state to an initial default.
*/
var resetState = function() {
state = {};
var date = Date();
state.cycle = {
status: Statuses.NOT_STARTED,
lastUpdate: date,
startTime: date
};
state.accounts = {};
var customerIds = getCustomerIdsPopulation();
for (var i = 0; i < customerIds.length; i++) {
state.accounts[customerIds[i]] = {
status: Statuses.NOT_STARTED,
lastUpdate: date
};
}
};
/**
* Gets the status of the current cycle.
*
* @return {string} The status of the current cycle.
*/
var getStatus = function() {
return state.cycle.status;
};
/**
* Sets the status of the current cycle.
*
* @param {string} status The status of the current cycle.
*/
var setStatus = function(status) {
var date = Date();
if (status == Statuses.IN_PROGRESS &&
state.cycle.status == Statuses.NOT_STARTED) {
state.cycle.startTime = date;
}
state.cycle.status = status;
state.cycle.lastUpdate = date;
};
/**
* Gets the start time of the current cycle.
*
* @return {Object} Date object for the start of the last cycle.
*/
var getLastStartTime = function() {
return new Date(state.cycle.startTime);
};
/**
* Gets accounts in the current cycle with a particular status.
*
* @param {string} status The status of the accounts to get.
* If null, all accounts are retrieved.
* @return {Array.<string>} A list of matching customerIds.
*/
var getAccountsWithStatus = function(status) {
var customerIds = [];
for (var customerId in state.accounts) {
if (!status || state.accounts[customerId].status == status) {
customerIds.push(customerId);
}
}
return customerIds;
};
/**
* Sets accounts in the current cycle with a particular status.
*
* @param {Array.<string>} customerIds A list of customerIds.
* @param {string} status A status to apply to those customerIds.
*/
var setAccountsWithStatus = function(customerIds, status) {
var date = Date();
for (var i = 0; i < customerIds.length; i++) {
var customerId = customerIds[i];
if (state.accounts[customerId]) {
state.accounts[customerId].status = status;
state.accounts[customerId].lastUpdate = date;
}
}
};
/**
* Registers the processing of a particular account with a result.
*
* @param {string} customerId The account that was processed.
* @param {{ * status: string,
* returnValue: Object
* error: string
* }} result The object to save for that account.
*/
var setAccountWithResult = function(customerId, result) {
if (state.accounts[customerId]) {
state.accounts[customerId].status = result.status;
state.accounts[customerId].returnValue = result.returnValue;
state.accounts[customerId].error = result.error;
state.accounts[customerId].lastUpdate = Date();
}
};
/**
* Gets the current results of the cycle for all accounts.
*
* @return {Object.<string, {
* status: string,
* lastUpdate: string,
* returnValue: Object,
* error: string
* }>} The results processed by the script during the cycle,
* keyed by account.
*/
var getResults = function() {
return state.accounts;
};
return {
loadState: loadState,
saveState: saveState,
resetState: resetState,
getStatus: getStatus,
setStatus: setStatus,
getLastStartTime: getLastStartTime,
getAccountsWithStatus: getAccountsWithStatus,
setAccountsWithStatus: setAccountsWithStatus,
setAccountWithResult: setAccountWithResult,
getResults: getResults
};
})();
/***************** END OF STANDARD TEMPLATE *****************/