Account Auditor - Manager Account

This is a Manager Account script. For operating on a single account, use the Single Account version of the script.

The Account Auditor script for AdWords manager accounts extends the single account Account Auditor script to run for multiple accounts under a manager account.

Many advertisers prefer to organize their accounts with a consistent structure. For example, some advertisers structure each campaign to have a fixed number of ad groups and each ad group to have a fixed number of ads and keywords. Other advertisers use labels to organize their accounts, where every campaign or ad group should have at least one label from a short list. Still others may organize their ad groups or ads so that only one ad group or ad is meant to be enabled at a given time. Ensuring that your account remains consistent with your desired structure can require time-consuming manual work.

Account Auditor can help in verifying the structure of your campaigns, ad groups, ads, and keywords. You define your intended structure using flexible "rules" in a spreadsheet, and the script analyzes your account and reports any entities that failed to satisfy your rules on a separate tab of the spreadsheet.

Account Auditor's functionality is similar to Automated Rules in AdWords, a powerful feature that lets advertisers make changes to their account or send email alerts based on rules. The primary difference is that Account Auditor allows you to check structural conditions on your accounts, such as that every enabled campaign has at least a minimum number of enabled ad groups, whereas Automated Rules focuses primarily on performance metrics.

Configuration

Basics

You configure Account Auditor by specifying rules in a spreadsheet. You should make a copy of this template spreadsheet and replace, modify, or augment the sample rules with your own.

The example above demonstrates several key features:

  • Each row represents a single rule. A rule has three parts: a unique ID (required), a trigger (optional), and a test (required).
  • A trigger or test both include an entity type (required) and conditions (optional). A test also indicates the expected number of test entities that must satisfy the test conditions (required).
  • A trigger or test can have multiple conditions by placing each condition on its own line within the same cell (not in a separate row).
  • A test can specify that all entities are expected to satisfy the conditions or that a simple numerical condition should be met, e.g., >5 entities.

Rules

In general, a row can be interpreted as follows: "For each Trigger Entity Type matching the Trigger Conditions, the Expected # of its associated Test Entity Type entities should match the Test Conditions". You can think of the trigger as a filter that determines which entities the test applies to. For example, the rules defined in the example above are listed below.

  • IDs 1, 2, 3: Each enabled campaign must have at least two enabled ad groups. Each enabled ad group must have at least five enabled ads. Finally, each enabled ad group must have at least ten enabled keywords.
  • ID 4: Each enabled campaign must have a non-zero budget.
  • ID 5: Each enabled ad group with the label "Always Run" must be part of an enabled campaign.
  • ID 6: Each enabled text ad whose headline contains "shoes" must be associated with (i.e., part of the same ad group as) keywords all of which are enabled and contain "shoes".
  • ID 7: Each enabled expanded text ad whose first headline part contains "shoes" must be associated with (i.e., part of the same ad group as) keywords all of which are enabled and contain "shoes".
  • ID 8: Each keyword with a quality score less than 3 must not be enabled (i.e., all of them are not enabled).
  • ID 9: All keywords must contain "My Brand".

Note that the trigger is optional. When omitted, as in rule ID 9, the test applies to all entities of the test entity's type.

Format

The conditions for a trigger or test must follow the format of its entity type's withCondition(condition) method, as in the following table:

Entity Type Conditions Format
Campaign CampaignSelector.withCondition(condition)
AdGroup AdGroupSelector.withCondition(condition)
Ad AdSelector.withCondition(condition)
Keyword KeywordSelector.withCondition(condition)

Only conditions on entity attributes are supported. Conditions on stats columns are not supported.

Multiple conditions can be specified by placing each condition on its own line within the same cell (not in a separate row). Multiple conditions are AND-ed together.

To add a new line within a cell, press Ctrl + Enter or Option + Enter while editing the cell contents.

The expected number of test entities satisfying the test conditions must follow a simple format:

  • The special word All means that all test entities must satisfy the test conditions.
  • A number means exactly that many test entities must satisfy the test conditions. The number must be an integer greater than or equal to zero.
  • Simple expressions of the form Operator Number, such as >= 3 or != 0, can be used. Allowed operators are =, !=, <, >, <=, and >=. Note that using the = operator is equivalent to omitting the operator.

If the trigger entity type is the same as or more specific than the test entity type, the expected number specified in the test must be All.

Scheduling

Schedule the script to run as often as you make significant structural changes to your account, such as creating, removing, enabling, or pausing campaigns, ad groups, ads, and keywords, on entities that you organize in a specific way. If you make structural changes to your account infrequently, scheduling the script Weekly is probably sufficient. If you make structural changes to your account frequently, either yourself or through other scripts, you may want to schedule the script Daily or even Hourly.

How it works

The script loads and parses each rule in the spreadsheet one by one, stopping at the first row that does not have an ID. It then checks each rule one at a time by retrieving and examining the relevant entities. Note that the trigger entity type can be broader than, narrower than, a sibling of, or the same as the test entity type. The script understands the AdWords entity hierarchy and applies each rule appropriately.

The script makes use of associative indexing to flexibly obtain the needed methods on AdWords entities. For example, the expression in the script trigger[testInfo.selector](), where trigger is an AdWords script entity and testInfo.selector is the name of a method to get a selector such as campaigns or adGroups, is equivalent to the more familiar trigger.campaigns() or trigger.adGroups(). This makes use of the fact that methods are named consistently throughout AdWords scripts for all entity types.

If the script finds any rule violations, it outputs the details to the Reports tab of the spreadsheet and sends an email alert to a list of recipients. The output on the Reports tab shows the specific entity that triggered the rule and the reason the test failed. The reason is either a specific entity that did not satisfy the test conditions (when All test entities are expected to satisfy the test conditions) or the number of entities actually satisfying the test conditions (when the expected number is an expression).

The script uses executeInParallel to process up to 50 accounts in parallel. To process more accounts, you can create multiple instances of the script and set the ACCOUNT_LABEL differently in each instance. Alternatively, you can use it with the Large Manager Hierarchy Template to operate on large numbers of accounts.

Setup

  • Set up a script with the source code below. Use a copy of this template spreadsheet.
  • Add rules to the spreadsheet as described above under Configuration.
  • Don't forget to update SPREADSHEET_URL and RECIPIENT_EMAILS in your script.
  • Schedule the script.

Source code

// 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 Account Auditor - Manager Account
 *
 * @overview The Account Auditor script helps you keep your accounts
 *     organized as you intend by automatically checking many types of
 *     structural conditions on your campaigns, ad groups, ads, and keywords.
 *     You define your intended structure using flexible "rules" in a
 *     spreadsheet, and the script analyzes your account and reports any
 *     entities that failed to satisfy your rules on a separate tab of the
 *     spreadsheet. See
 *     https://developers.google.com/adwords/scripts/docs/solutions/mccapp-account-auditor
 *     for more details.
 *
 * @author AdWords Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 1.1.1
 *
 * @changelog
 * - version 1.1.1
 *   - Added validation for external spreadsheet setup.
 * - version 1.1
 *   - Added support for different ad types, including expanded text ads.
 * - version 1.0.1
 *   - Improvements to time zone handling. Minor bug fixes.
 * - version 1.0
 *   - Released initial version.
 */

var CONFIG = {
  // URL of the spreadsheet template.
  // This should be a copy of https://goo.gl/PW5s4b.
  SPREADSHEET_URL: 'YOUR_SPREADSHEET_URL',

  // Whether to output results to a copy of the above spreadsheet (true) or to
  // the spreadsheet directly, overwriting previous results (false).
  COPY_SPREADSHEET: false,

  // Array of addresses to be alerted via email if rule failures are found.
  RECIPIENT_EMAILS: [
    'YOUR_EMAIL_HERE'
  ],

  ACCOUNT_LABEL: ''
};

// See applyRule() below for examples of each type.
var RULE_TYPES = {
  NO_TRIGGER: 'Rule applies to all entities of test entity type',
  BROAD_TRIGGER: 'Trigger entity type is broader than test entity type',
  NARROW_TRIGGER: 'Test entity type is broader than trigger entity type',
  SAME_TYPE: 'Trigger and test entity types are the same',
  SIBLING_TYPE: 'Trigger and test entity types are different but the same level'
};

// Static information for each type of entity.
// The keys in this table match the drop-down values in the spreadsheet and the
// values returned by getEntityType().
// selector: Name of the method used by a higher-level entity to retrieve a
//     selector for its descendant entities of this type.
// getter: Name of the method used by a lower-level entity to retrieve its
//     ancestor entity of this type.
// asText: Either:
//     (a) A string representing the method for this entity which returns a text
//         representation, OR
//     (b) A function, which when passed the entity as a single argument,
//         returns a text representation.
// level: Where the entity falls in the entity hierarchy. Lower numbers
//     represent broader entity types.
// parentType: The type of the entity's parent.
var ENTITY_TYPE_INFO = {
  Campaign: {
    selector: 'campaigns',
    getter: 'getCampaign',
    asText: 'getName',
    level: 1
  },
  AdGroup: {
    selector: 'adGroups',
    getter: 'getAdGroup',
    asText: 'getName',
    level: 2,
    parentType: 'Campaign'
  },
  Ad: {
    selector: 'ads',
    asText: getAdAsText,
    level: 3,
    parentType: 'AdGroup'
  },
  Keyword: {
    selector: 'keywords',
    asText: 'getText',
    level: 3,
    parentType: 'AdGroup'
  }
};

function main() {
  var accountSelector = MccApp.accounts();

  if (CONFIG.ACCOUNT_LABEL) {
    accountSelector = accountSelector
      .withCondition('LabelNames CONTAINS "' + CONFIG.ACCOUNT_LABEL + '"');
  }

  accountSelector.executeInParallel('processAccount', 'processResults');
}

/**
 * Finds failures and returns the results in a format suitable for
 * executeInParallel().
 *
 * @return {string} JSON stringified list of failure objects.
 */
function processAccount() {
  return JSON.stringify(findFailures());
}

/**
 * Outputs failures to a spreadsheet and sends an email alert if applicable.
 *
 * @param {Array.<Object>} results A list of ExecutionResult objects.
 */
function processResults(results) {
  var hasFailures = false;
  var spreadsheet = validateAndGetSpreadsheet(CONFIG.SPREADSHEET_URL);
  if (CONFIG.COPY_SPREADSHEET) {
    spreadsheet = spreadsheet.copy('Account Auditor');
  }
  initializeSpreadsheet(spreadsheet);

  for (var i = 0; i < results.length; i++) {
    if (!results[i].getError()) {
      hasFailures = outputFailures(spreadsheet, results[i].getCustomerId(),
          JSON.parse(results[i].getReturnValue())) || hasFailures;
    } else {
      Logger.log('Processing for ' + results[i].getCustomerId() + ' failed.');
    }
  }

  if (hasFailures) {
    validateEmailAddresses();
    sendEmail(spreadsheet);
  }
}

/**
 * Checks the account against all of the rules in the spreadsheet.
 *
 * @return {Array.<Object>} A list of failures found.
 */
function findFailures() {
  var rules = loadRules();
  var failures = [];

  for (var i = 0; i < rules.length; i++) {
    failures = failures.concat(applyRule(rules[i]));
  }

  return failures;
}

/**
 * Sets up the spreadsheet to receive output.
 *
 * @param {Object} spreadsheet The spreadsheet object.
 */
function initializeSpreadsheet(spreadsheet) {
  // Make sure the spreadsheet is using the account's timezone.
  spreadsheet.setSpreadsheetTimeZone(AdWordsApp.currentAccount().getTimeZone());

  // Clear the last run date on the spreadsheet.
  spreadsheet.getRangeByName('RunDate').clearContent();

  // Clear all rows in the Report tab of the spreadsheet below the header row.
  var outputRange = spreadsheet.getRangeByName('ReportHeaders')
    .offset(1, 0, spreadsheet.getSheetByName('Report')
        .getDataRange().getLastRow())
    .clearContent();
}

/**
 * Saves failures to a spreadsheet if present.
 *
 * @param {Object} spreadsheet The spreadsheet object.
 * @param {string} customerId The account the failures are for.
 * @param {Array.<Object>} failures A list of failures.
 * @return {boolean} True if there were failures and false otherwise.
 */
function outputFailures(spreadsheet, customerId, failures) {
  if (failures.length > 0) {
    saveFailuresToSpreadsheet(spreadsheet, customerId, failures);
    Logger.log('Rule failures were found for ' + customerId +
               '. See ' + spreadsheet.getUrl());
    return true;
  } else {
    Logger.log('No rule failures were found for ' + customerId + '.');
    return false;
  }
}

/**
 * Saves failures for a particular account to the spreadsheet starting at the
 * first unused row.
 *
 * @param {Object} spreadsheet The spreadsheet object.
 * @param {string} customerId The account that the failures are for.
 * @param {Array.<Object>} failures A list of failure objects.
 */
function saveFailuresToSpreadsheet(spreadsheet, customerId, failures) {
  // Find the first open row on the Report tab below the headers and create a
  // range large enough to hold all of the failures, one per row.
  var lastRow = spreadsheet.getSheetByName('Report')
    .getDataRange().getLastRow();
  var headers = spreadsheet.getRangeByName('ReportHeaders');
  var outputRange = headers
    .offset(lastRow - headers.getRow() + 1, 0, failures.length);

  // Build each row of output values in the order of the Report tab columns.
  var outputValues = [];
  for (var i = 0; i < failures.length; i++) {
    var failure = failures[i];
    outputValues.push([
      customerId,
      failure.ruleId,
      '',                               // blank column
      failure.trigger.entityType,
      failure.trigger.entityId,
      failure.trigger.entityText,
      '',                               // blank column
      failure.test.entityType,
      failure.test.entityId || '',
      failure.test.entityText || '',

      // Include a leading apostrophe to ensure test expressions like "= 0" are
      // not mistakenly treated as formulas.
      failure.test.expected && ('\'' + failure.test.op + ' ' +
                                failure.test.expected) || '',
      failure.test.actual || ''
    ]);
  }
  outputRange.setValues(outputValues);

  spreadsheet.getRangeByName('RunDate').setValue(new Date());

  for (var i = 0; i < CONFIG.RECIPIENT_EMAILS.length; i++) {
    spreadsheet.addEditor(CONFIG.RECIPIENT_EMAILS[i]);
  }
}

/**
 * Sends an email alert that failures were found.
 *
 * @param {Object} spreadsheet The spreadsheet object.
 */
function sendEmail(spreadsheet) {
  MailApp.sendEmail(CONFIG.RECIPIENT_EMAILS.join(','),
      'Account Auditor Script: Rule Failures Were Found',
      'The Account Auditor Script found cases where the entities in your ' +
      'AdWords account(s) did not meet your rules. ' +
      'See the Report tab of ' + spreadsheet.getUrl() + ' for details.');
}

/**
 * Loads the rules from the spreadsheet.
 *
 * @return {Array.<Object>} A list of rule objects.
 */
function loadRules() {
  var rules = [];
  var spreadsheet = validateAndGetSpreadsheet(CONFIG.SPREADSHEET_URL);
  var lastRow = spreadsheet.getSheetByName('Rules').getDataRange().getLastRow();

  // Get all of the rows below the header row.
  var ruleData = spreadsheet.getRange('RuleHeaders')
      .offset(1, 0, lastRow).getValues();

  var ruleIds = {};
  var i = 0;

  while (ruleData[i][0]) {
    var rule = parseRule(ruleData[i]);
    rules.push(rule);

    // Rule IDs must be unique.
    if (!ruleIds[rule.id]) {
      ruleIds[rule.id] = true;
    } else {
      throw 'Multiple rules with ID ' + rule.id + '. Check the spreadsheet ' +
        'to make sure every rule has a unique ID.';
    }

    Logger.log('Loaded rule ' + rule.id + '.');
    i++;
  }

  Logger.log('Total number of rules: ' + i + '.');
  return rules;
}

/**
 * Parses a row from the spreadsheet into a rule object.
 *
 * @param {Array} ruleData A row from the spreadsheet.
 * @return {Object} A rule object.
 */
function parseRule(ruleData) {
  var rule = {
    id: ruleData[0].toString()
  };

  if (!rule.id) {
    throw 'Rule missing ID. Check the spreadsheet to make sure every rule ' +
        'has a unique ID.';
  }

  // Tests are required.
  if (!ruleData[5]) {
    throw 'Rule ' + rule.id + ' test is missing entity type.';
  } else {
    rule.test = {
      entityType: ruleData[5],
      conditions: ruleData[6] && ruleData[6].split('\n')
    };
  }

  // Triggers are optional, but it is invalid to have a condition without an
  // entity type.
  if (ruleData[3] && !ruleData[2]) {
    throw 'Rule ' + rule.id + ' trigger has conditions without an entity type.';
  } else if (ruleData[2]) {
    rule.trigger = {
      entityType: ruleData[2],
      conditions: ruleData[3] && ruleData[3].split('\n')
    };
  }

  // Count expressions are required.
  parsedExpr = parseCountExpr(ruleData[7]);
  if (!parsedExpr) {
    throw 'Rule ' + rule.id + ' has invalid expression for expected number.';
  }
  rule.test.op = parsedExpr.op;
  rule.test.num = parsedExpr.num;

  rule.type = getRuleType(rule);

  // Certain rule types can only use 'All'.
  if ((rule.type == RULE_TYPES.NARROW_TRIGGER ||
       rule.type == RULE_TYPES.SAME_TYPE) &&
      rule.test.op != 'All') {
    throw 'Rule ' + rule.id + ' must use "All" for the expected number.';
  }

  return rule;
}

/**
 * Parses a simple relational expression.
 *
 * @param {string} expr An expression of the form 'Op Num', where Op is one
 *     of '=', '!=', '<', '>', '<=', or '>=' and Num is a non-negative integer
 *     or the special expression 'All'. If Op is omitted it is assumed to be
 *     '='.
 * @return {Object} The parsed Op and Num, or undefined if the parse failed.
 */
function parseCountExpr(expr) {
  expr = expr.toString();

  // Check for the special expression 'All'.
  if (expr.match(/^\s*All\s*$/i)) {
    return {
      op: 'All'
    };
  }

  // If the operator is missing, prepend '=' as the default operator.
  if (expr.match(/^\s*\d*\s*$/)) {
    expr = '=' + expr;
  }

  var regex = /^\s*(\!?\=|\<\=?|\>\=?)\s*(\d*)\s*$/;
  var result = regex.exec(expr);

  if (result) {
    return {
      op: result[1],
      num: result[2]
    };
  }
}

/**
 * Determines the type of rule evaluation strategy to apply.
 *
 * @param {Object} rule A rule object.
 * @return {string} The type of rule evaluation strategy to apply.
 */
function getRuleType(rule) {
  if (!rule.trigger) {
    return RULE_TYPES.NO_TRIGGER;
  } else if (ENTITY_TYPE_INFO[rule.test.entityType].level >
      ENTITY_TYPE_INFO[rule.trigger.entityType].level) {
    return RULE_TYPES.BROAD_TRIGGER;
  } else if (ENTITY_TYPE_INFO[rule.test.entityType].level <
      ENTITY_TYPE_INFO[rule.trigger.entityType].level) {
    return RULE_TYPES.NARROW_TRIGGER;
  } else if (rule.test.entityType == rule.trigger.entityType) {
    return RULE_TYPES.SAME_TYPE;
  } else {
    return RULE_TYPES.SIBLING_TYPE;
  }
}

/**
 * Retrieves a text representation of an ad, casting the ad to the appropriate
 * type if necessary.
 *
 * @param {Ad} ad The ad object.
 * @return {string} The text representation.
 */
function getAdAsText(ad) {
  // There is no AdTypeSpace method for textAd
  if (ad.getType() === 'TEXT_AD') {
    return ad.getHeadline();
  } else if (ad.isType().expandedTextAd()) {
    var eta = ad.asType().expandedTextAd();
    return eta.getHeadlinePart1() + ' - ' + eta.getHeadlinePart2();
  } else if (ad.isType().gmailImageAd()) {
    return ad.asType().gmailImageAd().getName();
  } else if (ad.isType().gmailMultiProductAd()) {
    return ad.asType().gmailMultiProductAd().getHeadline();
  } else if (ad.isType().gmailSinglePromotionAd()) {
    return ad.asType().gmailSinglePromotionAd().getHeadline();
  } else if (ad.isType().html5Ad()) {
    return ad.asType().html5Ad().getName();
  } else if (ad.isType().imageAd()) {
    return ad.asType().imageAd().getName();
  } else if (ad.isType().responsiveDisplayAd()) {
    return ad.asType().responsiveDisplayAd().getLongHeadline();
  }
  return 'N/A';
}

/**
 * Finds all cases where entities in the account fail to match a rule.
 *
 * @param {Object} rule A rule object.
 * @return {Array.<Object>} A list of failure objects, each describing a case
 *     where the rule was not met.
 */
function applyRule(rule) {
  Logger.log('Applying rule ' + rule.id + '.');

  var failures = [];

  var testInfo = ENTITY_TYPE_INFO[rule.test.entityType];

  // Get the trigger entities.
  if (rule.type != RULE_TYPES.NO_TRIGGER) {
    var triggerInfo = ENTITY_TYPE_INFO[rule.trigger.entityType];
    var triggerSelector = AdWordsApp[triggerInfo.selector]();
    addConditions(triggerSelector, rule.trigger.conditions);
    var triggers = triggerSelector.get();
  }

  // Helper method to get details about the entity associated with a failure.
  var getEntityDetails = function(entity) {
    var entityType = entity && entity.getEntityType();

    if (entityType) {
      var text = 'N/A';
      var fn = ENTITY_TYPE_INFO[entityType].asText;

      if (typeof fn === 'string') {
        // Specified as a string method name to get a text representation.
        text = entity[fn]();
      } else if (typeof fn === 'function') {
        // Specified as a function, to which the entity is passed to extract a
        // text representation. Used in the case of Ads, which can have a number
        // of underlying types.
        text = fn(entity);
      }
    }

    return {
      entityId: entity ? entity.getId() : 'N/A',
      entityType: entity ? entityType : 'N/A',
      entityText: text ? text : 'N/A'
    };
  };

  // Helper method to build failures for each entity in a selector that does
  // not match the test conditions.
  var checkAll = function(params) {
    var entities = findMissingEntities(params.selector, rule.test.conditions);

    for (var i = 0; i < entities.length; i++) {
      var entity = entities[i];

      // The trigger is either provided as a map, indicated to be the entity
      // itself, or provided directly (possibly null if there is no trigger).
      var trigger = params.triggerMap && params.triggerMap[entity.getId()] ||
        (params.trigger == 'entity' && entity || params.trigger);

      failures.push({
        ruleId: rule.id,
        trigger: getEntityDetails(trigger),
        test: getEntityDetails(entity)
      });
    }
  };

  // Helper method to build failures where the number of entities in a selector
  // does not match the test's expected number.
  var checkCount = function(params) {
    addConditions(params.selector, rule.test.conditions);
    var expected = rule.test.num;
    var actual = params.selector.get().totalNumEntities();
    var op = rule.test.op || '=';

    if (!compare(actual, expected, op)) {
      failures.push({
        ruleId: rule.id,
        trigger: getEntityDetails(params.trigger),
        test: {
          entityType: rule.test.entityType,
          expected: expected,
          op: op,
          actual: actual
        }
      });
    }
  };

  // Helper method to use the appropriate checker depending on the operator.
  var checkRule = function(params) {
    if (rule.test.op == 'All') {
      checkAll(params);
    } else {
      checkCount(params);
    }
  };

  switch (rule.type) {
    case RULE_TYPES.NO_TRIGGER:
      // Example: All campaigns are enabled (test).
      // Example: There are X enabled campaigns (test).
      checkRule({selector: AdWordsApp[testInfo.selector]()});
      break;

    case RULE_TYPES.NARROW_TRIGGER:
      // Example: For each enabled ad group (trigger), its campaign is
      // enabled (test).
      var testIds = [];
      var triggerMap = {};

      while (triggers.hasNext()) {
        var trigger = triggers.next();
        var testId = trigger[testInfo.getter]().getId();
        testIds.push(testId);
        triggerMap[testId] = trigger;
      }

      checkAll({
        triggerMap: triggerMap,
        selector: AdWordsApp[testInfo.selector]().withIds(testIds)
      });
      break;

    case RULE_TYPES.BROAD_TRIGGER:
      // Example: For each enabled campaign (trigger), all of its ad
      // groups are enabled (test).
      // Example: For each enabled campaign (trigger), it has at least X
      // enabled ad groups (test).
      while (triggers.hasNext()) {
        var trigger = triggers.next();
        var testSelector = trigger[testInfo.selector]();
        checkRule({trigger: trigger, selector: testSelector});
      }
      break;

    case RULE_TYPES.SAME_TYPE:
      // Example: For each ad group with label X (trigger), it is enabled
      // (test).
      checkAll({trigger: 'entity', selector: triggerSelector});
      break;

    case RULE_TYPES.SIBLING_TYPE:
      // Example: For each enabled ad (trigger), it is associated with at least
      // X approved keywords (test).
      var parentIds = {};

      while (triggers.hasNext()) {
        var trigger = triggers.next();
        var parent = trigger[ENTITY_TYPE_INFO[triggerInfo.parentType].getter]();
        var parentId = parent.getId();

        // If we have seen the parent already, it is unnecessary and inefficient
        // to check again since the siblings are the same. This means only the
        // first sibling will be listed as the trigger for any failures.
        if (!parentIds[parentId]) {
          parentIds[parentId] = true;
          var testSelector = parent[testInfo.selector]();
          checkRule({trigger: trigger, selector: testSelector});
        }
      }
      break;
  }

  return failures;
}

/**
 * Adds a set of conditions to a selector. The provided selector is modified.
 *
 * @param {Object} selector An AdWords scripts selector.
 * @param {Array.<string>} conditions An array of conditions to be applied to
 *     the selector using withCondition().
 */
function addConditions(selector, conditions) {
  if (conditions) {
    for (var i = 0; i < conditions.length; i++) {
      selector = selector.withCondition(conditions[i]);
    }
  }
}

/**
 * Retrieves the IDs of a set of AdWords scripts entities.
 *
 * @param {Object} iterator An iterator over an AdWords scripts entity that
 *     has a getId() method.
 * @return {Array.<number>} An array of IDs of the entities.
 */
function getEntityIds(iterator) {
  var ids = [];

  while (iterator.hasNext()) {
    ids.push(iterator.next().getId());
  }

  return ids;
}

/**
 * Identifies entities in a selector that do not match a set of conditions.
 * Modifies the given selector.
 *
 * @param {Object} selector An AdWords scripts selector for an entity type.
 *     that has a getId() method.
 * @param {Array.<string>} conditions An array of conditions to be applied to
 *     the selector using withCondition().
 * @return {Array.<Object>} A list of AdWords objects that did not meet the
 *     conditions.
 */
function findMissingEntities(selector, conditions) {
  var missing = [];

  // Get an iterator *before* applying the conditions.
  var entities = selector.get();

  // Get the IDs of the entities matching the conditions.
  addConditions(selector, conditions);
  var ids = getEntityIds(selector.get());

  while (entities.hasNext()) {
    var entity = entities.next();
    if (ids.indexOf(entity.getId()) == -1) {
      missing.push(entity);
    }
  }

  return missing;
}

/**
 * Compares two numbers using a given operator.
 *
 * @param {number} val1 The first number in the comparison.
 * @param {number} val2 The second number in the comparison.
 * @param {string} op The operator for the comparison.
 * @return {boolean} The result of the comparison 'val1 op val2'.
 */
function compare(val1, val2, op) {
  switch (op) {
    case '=':
      return val1 == val2;
    case '!=':
      return val1 != val2;
    case '<':
      return val1 < val2;
    case '>':
      return val1 > val2;
    case '<=':
      return val1 <= val2;
    case '>=':
      return val1 >= val2;
  }
}

/**
 * DO NOT EDIT ANYTHING BELOW THIS LINE.
 * Please modify your spreadsheet URL and email addresses at the top of the file
 * only.
 */

/**
 * Validates the provided spreadsheet URL and email address
 * to make sure that they're set up properly. Throws a descriptive error message
 * if validation fails.
 *
 * @param {string} spreadsheeturl The URL of the spreadsheet to open.
 * @return {Spreadsheet} The spreadsheet object itself, fetched from the URL.
 * @throws {Error} If the spreadsheet URL or email hasn't been set
 */
function validateAndGetSpreadsheet(spreadsheeturl) {
  if (spreadsheeturl == 'YOUR_SPREADSHEET_URL') {
    throw new Error('Please specify a valid Spreadsheet URL. You can find' +
        ' a link to a template in the associated guide for this script.');
  }
  var spreadsheet = SpreadsheetApp.openByUrl(spreadsheeturl);
  return spreadsheet;
}

/**
 * Validates the provided email address to make sure it's not the default.
 * Throws a descriptive error message if validation fails.
 *
 * @throws {Error} If the list of email addresses is still the default
 */
function validateEmailAddresses() {
  if (CONFIG.RECIPIENT_EMAILS &&
      CONFIG.RECIPIENT_EMAILS[0] == 'YOUR_EMAIL_HERE') {
    throw new Error('Please specify a valid email address.');
  }
}

    

Send feedback about...

AdWords Scripts
AdWords Scripts
Need help? Visit our support page.