帐号审核工具 - 单一帐号

This script is for a single account. For operating on multiple accounts in a Manager Account, use the Manager Account version of the script.

很多广告客户希望用一致的结构来组织自己的帐号。例如,一些广告客户所制作的每个广告系列都有固定数量的广告组,每个广告组都有固定数量的广告和关键字。另有一些广告客户使用标签来组织自己的帐号,其中的每个广告系列或广告组应该至少从某个简表中获得一个标签。还有一些人可能将其广告组或广告安排为,在某个给定时间只会启用一个广告组或广告。确保您的帐号与所需结构保持一致,可能需要完成耗时的人工工作。

帐号审核工具可以帮助您验证您的广告系列、广告组、广告和关键字的结构。您在电子表格中采用灵活的“规则”定义所需结构,而脚本会分析您的帐号,并在电子表格一个单独的标签上报告未能满足您的规则的任何实体。

帐号审核工具的功能类似于 AdWords 中的自动规则:自动规则功能非常强大,可让广告客户更改自己的帐号或基于规则发送电子邮件提醒。两者主要的区别在于帐号审核工具允许您检查帐号的结构条件,如每一个已启用的广告系列至少有最低数量的已启用广告组,而自动规则主要侧重于效果指标。

配置

基础知识

通过在电子表格中指定规则配置帐号审核工具。您应该制作此电子表格模板的副本,并用自己的规则来替换、修改或增强示例规则。

上述示例演示了几个关键功能:

  • 每一行代表一个规则。规则有三个部分:一个唯一的 ID(必需)、触发器(可选)以及测试(必需)。
  • 触发器或测试都包括实体类型(必需)和条件(可选)。测试还指出必须满足测试条件的测试实体的预期数量(必需)。
  • 触发器或测试可以有多个条件,方法是,将每个条件置于同一单元格内自身的行中(而不是在一个单独的表格行中)。
  • 测试可以指定是要求所有实体满足条件,还是应该满足一个简单的数值条件,例如,>5 个实体。

规则

一般情况下,对表格行可作以下解释:“对于每个匹配触发条件的触发器实体类型,其相关联的测试实体类型实体的预期数量应该匹配测试条件”。可以把触发器看作是决定要应用测试的实体的过滤条件。例如,下面列出了在上述例子中定义的规则。

  • ID 1、2、3:每个已启用的广告系列必须至少有两个已启用的广告组。每个已启用的广告组必须至少有五个已启用的广告。最后,每个已启用的广告组必须至少有十个已启用的关键字。
  • ID 4:每个已启用的广告系列都必须有非零预算。
  • ID 5:每个已启用的带有标签“始终投放”的广告组必须属于已启用的广告系列。
  • ID 6:每个已启用的标题中包含“鞋”的文字广告都必须与(即同一广告组中)所有已启用且包含“鞋”的关键字相关联。
  • ID 7:质量得分小于 3 的关键字不得启用(即,启用数目是零)。
  • ID 8:所有广告都必须是文字广告。

请注意,该触发器是可选的。例如,如果规则 ID 8 被忽略,则测试将应用于测试实体类型的所有实体。

格式

如下表所示,触发器或测试的条件必须遵循其实体类型 withCondition(condition) 方法的格式:

实体类型 条件格式
Campaign(广告系列) CampaignSelector.withCondition(condition)
AdGroup(广告组) AdGroupSelector.withCondition(condition)
Ad(广告) AdSelector.withCondition(condition)
Keyword(关键字) KeywordSelector.withCondition(condition)

只支持实体属性上的条件,不支持统计信息列的条件。

可以指定多个条件,方法是,将每个条件置于同一单元格内自身的行中(而不是在一个单独的表格行中)。多个条件用 AND 组合在一起。

要在单元格中添加新行,请在修改单元格内容的同时按 Ctrl + EnterOption + Enter

满足测试条件的测试实体的预期数量必须遵循一个简单的格式:

  • 特殊词 All 意味着所有测试实体必须满足测试条件。
  • 数字是必须满足测试条件的测试实体的确切数量。该数字必须是大于或等于零的整数。
  • 可以使用 Operator Number 形式的简单表达式,如 >= 3!= 0。允许的运算符是 =!=<><=>=。请注意,使用 = 运算符相当于忽略了该运算符。

如果触发器实体类型与测试实体类型相同或比后者更具体,则在测试中指定的预期数量必须是 All

运行时间设置

将脚本在采用特定组织方式的实体上的运行频率安排为您对帐号做出显著的结构性更改的频率,这类更改包括创建、移除、启用或暂停广告系列、广告组、广告和关键字等。如果您不是频繁对帐号进行结构性更改,则安排脚本每周运行一次可能就足够了。如果您频繁对帐号进行结构性更改(无论是自行更改还是通过其他脚本),您可能要安排脚本每天甚至每小时运行一次。

工作原理

该脚本逐一加载并解析电子表格中的每一条规则,在不具有 ID 的第一行停止。然后,通过获取和检查相关的实体,一次检查一条规则。请注意,触发器实体类型可能与测试实体类型相同或者是后者的同级,或者范围更宽或更窄。该脚本理解 AdWords 实体的层次结构,并相应地应用每一条规则。

该脚本利用关联索引灵活地获取 AdWords 实体所需的方法。例如,在脚本 trigger[testInfo.selector]() 的表达中,trigger 是 AdWords 脚本实体,而 testInfo.selector 是获得 campaignsadGroups 等选择器的方法名称,与较为人熟知的 trigger.campaigns()trigger.adGroups() 等效。这利用了一个事实,即 AdWords 脚本对所有实体类型的方法命名都是统一的。

如果脚本发现任何违规,它会将细节输出到电子表格的 Reports(报告)标签,并向收件人列表发送电子邮件提醒。Reports(报告)标签上的输出显示触发规则的具体实体和测试失败的原因。问题要么是存在没有满足测试条件的特定实体(预期 All 测试实体满足测试条件时),要么出在实际满足测试条件的实体数目上(当表达式中给出预期数目时)。

设置

  • 使用下面的源代码设置脚本。使用此电子表格模板的副本。
  • 根据上述内容在配置下为电子表格添加规则。
  • 不要忘了在您的脚本中更新 SPREADSHEET_URLRECIPIENT_EMAILS
  • 安排脚本运行时间。

源代码

// 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
 *
 * @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/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'
  ]
};

// 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',
    // For ads, asText references a specific function, not a method name. For
    // details refer to the above JSDoc or @see getEntityDetails definition.
    asText: getAdAsText,
    level: 3,
    parentType: 'AdGroup'
  },
  Keyword: {
    selector: 'keywords',
    asText: 'getText',
    level: 3,
    parentType: 'AdGroup'
  }
};

function main() {
  var failures = findFailures();

  var spreadsheet = validateAndGetSpreadsheet(CONFIG.SPREADSHEET_URL);
  if (CONFIG.COPY_SPREADSHEET) {
    spreadsheet = spreadsheet.copy('Account Auditor');
  }
  initializeSpreadsheet(spreadsheet);

  var hasFailures = outputFailures(spreadsheet,
    AdWordsApp.currentAccount().getCustomerId(), failures);

  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;
}

/**
 * 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;
  }
}

/**
 * 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 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.');
  }
}

    

发送以下问题的反馈:

此网页
AdWords Scripts
AdWords Scripts
需要帮助?请访问我们的支持页面