SHARE
TWEET

Untitled

a guest May 16th, 2019 60 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // Copyright 2015, Google Inc. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. //     http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14.  
  15. /**
  16.  * @name Negative Keyword Conflicts
  17.  *
  18.  * @overview The Negative Keyword Conflicts script generates a spreadsheet
  19.  *     and email alert if a Google Ads account has positive keywords which are
  20.  *     blocked by negative keywords. See
  21.  *     https://developers.google.com/google-ads/scripts/docs/solutions/negative-keyword-conflicts
  22.  *     for more details.
  23.  *
  24.  * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
  25.  *
  26.  * @version 1.3.3
  27.  *
  28.  * @changelog
  29.  * - version 1.3.3
  30.  *   - Added column for negative keyword list name.
  31.  * - version 1.3.2
  32.  *   - Added validation for external spreadsheet setup.
  33.  * - version 1.3.1
  34.  *   - Fix bug where campaigns with multiple shared negative keyword lists were
  35.  *     not handled correctly.
  36.  * - version 1.3.0
  37.  *   - Fix bug where in certain cases phrase match negatives were incorrectly
  38.  *     reported as blocking positive keywords.
  39.  * - version 1.2.1
  40.  *   - Improvements to time zone handling.
  41.  * - version 1.2
  42.  *   - Improved compatibility with Large Manager Hierarchy template.
  43.  *   - Add option for reusing the spreadsheet or making a copy.
  44.  * - version 1.1
  45.  *   - Bug fixes.
  46.  * - version 1.0
  47.  *   - Released initial version.
  48.  */
  49.  
  50. var CONFIG = {
  51.   // URL of the spreadsheet template.
  52.   // This should be a copy of https://goo.gl/M4HjaH.
  53.   SPREADSHEET_URL: 'https://docs.google.com/spreadsheets/d/10TfpdqM2-q5q4waI9kmEgi5WpnuTuhjZU3PY3Yx4Sfs/edit#gid=0',
  54.  
  55.   // Whether to output results to a copy of the above spreadsheet (true) or to
  56.   // the spreadsheet directly, overwriting previous results (false).
  57.   COPY_SPREADSHEET: false,
  58.  
  59.   // Array of addresses to be alerted via email if conflicts are found.
  60.   RECIPIENT_EMAILS: [
  61.     'maksskam121@gmail.com'
  62.   ],
  63.  
  64.   // Label on the campaigns to be processed.
  65.   // Leave blank to include all campaigns.
  66.   CAMPAIGN_LABEL: '',
  67.  
  68.   // Limits on the number of keywords in an account the script can process.
  69.   MAX_POSITIVES: 250000,
  70.   MAX_NEGATIVES: 50000
  71. };
  72.  
  73. /**
  74.  * Configuration to be used for running reports.
  75.  */
  76. var REPORTING_OPTIONS = {
  77.   // Comment out the following line to default to the latest reporting version.
  78.   apiVersion: 'v201809'
  79. };
  80.  
  81. function main() {
  82.   var spreadsheet = validateAndGetSpreadsheet(CONFIG.SPREADSHEET_URL);
  83.   validateEmailAddresses();
  84.  
  85.   var conflicts = findAllConflicts();
  86.  
  87.   if (CONFIG.COPY_SPREADSHEET) {
  88.     spreadsheet = spreadsheet.copy('Negative Keyword Conflicts');
  89.   }
  90.   initializeSpreadsheet(spreadsheet);
  91.  
  92.   var hasConflicts = outputConflicts(spreadsheet,
  93.     AdsApp.currentAccount().getCustomerId(), conflicts);
  94.  
  95.   if (hasConflicts && CONFIG.RECIPIENT_EMAILS) {
  96.     sendEmail(spreadsheet);
  97.   }
  98. }
  99.  
  100. /**
  101.  * Finds all negative keyword conflicts in an account.
  102.  *
  103.  * @return {Array.<Object>} An array of conflicts.
  104.  */
  105. function findAllConflicts() {
  106.   var campaignIds;
  107.   if (CONFIG.CAMPAIGN_LABEL) {
  108.     campaignIds = getCampaignIdsWithLabel(CONFIG.CAMPAIGN_LABEL);
  109.   } else {
  110.     campaignIds = getAllCampaignIds();
  111.   }
  112.  
  113.   var campaignCondition = '';
  114.   if (campaignIds.length > 0) {
  115.     campaignCondition = 'AND CampaignId IN [' + campaignIds.join(',') + ']';
  116.   }
  117.  
  118.   Logger.log('Downloading keywords performance report');
  119.   var query =
  120.     'SELECT CampaignId, CampaignName, AdGroupId, AdGroupName, ' +
  121.     '       Criteria, KeywordMatchType, IsNegative ' +
  122.     'FROM KEYWORDS_PERFORMANCE_REPORT ' +
  123.     'WHERE CampaignStatus = "ENABLED" AND AdGroupStatus = "ENABLED" AND ' +
  124.     '      Status = "ENABLED" AND IsNegative IN [true, false] ' +
  125.     '      ' + campaignCondition + ' ' +
  126.     'DURING YESTERDAY';
  127.   var report = AdsApp.report(query, REPORTING_OPTIONS);
  128.  
  129.   Logger.log('Building cache and populating with keywords');
  130.   var cache = {};
  131.   var numPositives = 0;
  132.   var numNegatives = 0;
  133.  
  134.   var rows = report.rows();
  135.   while (rows.hasNext()) {
  136.     var row = rows.next();
  137.  
  138.     var campaignId = row['CampaignId'];
  139.     var campaignName = row['CampaignName'];
  140.     var adGroupId = row['AdGroupId'];
  141.     var adGroupName = row['AdGroupName'];
  142.     var keywordText = row['Criteria'];
  143.     var keywordMatchType = row['KeywordMatchType'];
  144.     var isNegative = row['IsNegative'];
  145.  
  146.     if (!cache[campaignId]) {
  147.       cache[campaignId] = {
  148.         campaignName: campaignName,
  149.         adGroups: {},
  150.         negatives: [],
  151.         negativesFromLists: [],
  152.       };
  153.     }
  154.  
  155.     if (!cache[campaignId].adGroups[adGroupId]) {
  156.       cache[campaignId].adGroups[adGroupId] = {
  157.         adGroupName: adGroupName,
  158.         positives: [],
  159.         negatives: [],
  160.       };
  161.     }
  162.  
  163.     if (isNegative == 'true') {
  164.       cache[campaignId].adGroups[adGroupId].negatives
  165.         .push(normalizeKeyword(keywordText, keywordMatchType));
  166.       numNegatives++;
  167.     } else {
  168.       cache[campaignId].adGroups[adGroupId].positives
  169.         .push(normalizeKeyword(keywordText, keywordMatchType));
  170.       numPositives++;
  171.     }
  172.  
  173.     if (numPositives > CONFIG.MAX_POSITIVES ||
  174.         numNegatives > CONFIG.MAX_NEGATIVES) {
  175.       throw 'Trying to process too many keywords. Please restrict the ' +
  176.             'script to a smaller subset of campaigns.';
  177.     }
  178.   }
  179.  
  180.   Logger.log('Downloading campaign negatives report');
  181.   var query =
  182.     'SELECT CampaignId, Criteria, KeywordMatchType ' +
  183.     'FROM CAMPAIGN_NEGATIVE_KEYWORDS_PERFORMANCE_REPORT ' +
  184.     'WHERE CampaignStatus = "ENABLED" ' +
  185.     '      ' + campaignCondition;
  186.   var report = AdsApp.report(query, REPORTING_OPTIONS);
  187.  
  188.   var rows = report.rows();
  189.   while (rows.hasNext()) {
  190.     var row = rows.next();
  191.  
  192.     var campaignId = row['CampaignId'];
  193.     var keywordText = row['Criteria'];
  194.     var keywordMatchType = row['KeywordMatchType'];
  195.  
  196.     if (cache[campaignId]) {
  197.       cache[campaignId].negatives
  198.         .push(normalizeKeyword(keywordText, keywordMatchType));
  199.     }
  200.   }
  201.  
  202.   Logger.log('Populating cache with negative keyword lists');
  203.   var negativeKeywordLists =
  204.     AdsApp.negativeKeywordLists().withCondition('Status = ACTIVE').get();
  205.  
  206.   while (negativeKeywordLists.hasNext()) {
  207.     var negativeKeywordList = negativeKeywordLists.next();
  208.  
  209.     var negativeList = {name: negativeKeywordList.getName(), negatives: []};
  210.     var negativeKeywords = negativeKeywordList.negativeKeywords().get();
  211.  
  212.     while (negativeKeywords.hasNext()) {
  213.       var negative = negativeKeywords.next();
  214.       negativeList.negatives.push(
  215.           normalizeKeyword(negative.getText(), negative.getMatchType()));
  216.     }
  217.  
  218.     var campaigns = negativeKeywordList.campaigns()
  219.         .withCondition('Status = ENABLED').get();
  220.  
  221.     while (campaigns.hasNext()) {
  222.       var campaign = campaigns.next();
  223.       var campaignId = campaign.getId();
  224.  
  225.       if (cache[campaignId]) {
  226.         cache[campaignId].negativesFromLists =
  227.             cache[campaignId].negativesFromLists.concat(negativeList);
  228.       }
  229.     }
  230.   }
  231.  
  232.   Logger.log('Finding negative conflicts');
  233.   var conflicts = [];
  234.  
  235.   // Adds context about the conflict.
  236.   var enrichConflict = function(
  237.       conflict, campaignId, adGroupId, level, opt_listName) {
  238.     conflict.campaignId = campaignId;
  239.     conflict.adGroupId = adGroupId;
  240.     conflict.campaignName = cache[campaignId].campaignName;
  241.     conflict.adGroupName = cache[campaignId].adGroups[adGroupId].adGroupName;
  242.     conflict.level = level;
  243.     conflict.listName = opt_listName || '-';
  244.   };
  245.  
  246.   for (var campaignId in cache) {
  247.     for (var adGroupId in cache[campaignId].adGroups) {
  248.       var positives = cache[campaignId].adGroups[adGroupId].positives;
  249.  
  250.       var negativeLevels = {
  251.         'Campaign': cache[campaignId].negatives,
  252.         'Ad Group': cache[campaignId].adGroups[adGroupId].negatives
  253.       };
  254.  
  255.       for (var level in negativeLevels) {
  256.         var newConflicts =
  257.           checkForConflicts(negativeLevels[level], positives);
  258.  
  259.         for (var i = 0; i < newConflicts.length; i++) {
  260.           enrichConflict(newConflicts[i], campaignId, adGroupId, level);
  261.         }
  262.         conflicts = conflicts.concat(newConflicts);
  263.       }
  264.  
  265.       var negativeLists = cache[campaignId].negativesFromLists;
  266.       var level = 'Negative list';
  267.       for (var k = 0; k < negativeLists.length; k++) {
  268.         var negativeList = negativeLists[k];
  269.         var newConflicts = checkForConflicts(negativeList.negatives, positives);
  270.  
  271.         for (var j = 0; j < newConflicts.length; j++) {
  272.           enrichConflict(
  273.               newConflicts[j], campaignId, adGroupId, level, negativeList.name);
  274.         }
  275.         conflicts = conflicts.concat(newConflicts);
  276.       }
  277.     }
  278.   }
  279.  
  280.   return conflicts;
  281. }
  282.  
  283. /**
  284.  * Saves conflicts to a spreadsheet if present.
  285.  *
  286.  * @param {Object} spreadsheet The spreadsheet object.
  287.  * @param {string} customerId The account the conflicts are for.
  288.  * @param {Array.<Object>} conflicts A list of conflicts.
  289.  * @return {boolean} True if there were conflicts and false otherwise.
  290.  */
  291. function outputConflicts(spreadsheet, customerId, conflicts) {
  292.   if (conflicts.length > 0) {
  293.     saveConflictsToSpreadsheet(spreadsheet, customerId, conflicts);
  294.     Logger.log('Conflicts were found for ' + customerId +
  295.                '. See ' + spreadsheet.getUrl());
  296.     return true;
  297.   } else {
  298.     Logger.log('No conflicts were found for ' + customerId + '.');
  299.     return false;
  300.   }
  301. }
  302.  
  303. /**
  304.  * Sets up the spreadsheet to receive output.
  305.  *
  306.  * @param {Object} spreadsheet The spreadsheet object.
  307.  */
  308. function initializeSpreadsheet(spreadsheet) {
  309.   // Make sure the spreadsheet is using the account's timezone.
  310.   spreadsheet.setSpreadsheetTimeZone(AdsApp.currentAccount().getTimeZone());
  311.  
  312.   // Clear the last run date on the spreadsheet.
  313.   spreadsheet.getRangeByName('RunDate').clearContent();
  314.  
  315.   // Clear all rows in the spreadsheet below the header row.
  316.   var outputRange = spreadsheet.getRangeByName('Headers')
  317.     .offset(1, 0, spreadsheet.getSheetByName('Conflicts')
  318.         .getDataRange().getLastRow())
  319.     .clearContent();
  320. }
  321.  
  322. /**
  323.  * Saves conflicts for a particular account to the spreadsheet starting at the
  324.  * first unused row.
  325.  *
  326.  * @param {Object} spreadsheet The spreadsheet object.
  327.  * @param {string} customerId The account that the conflicts are for.
  328.  * @param {Array.<Object>} conflicts A list of conflicts.
  329.  */
  330. function saveConflictsToSpreadsheet(spreadsheet, customerId, conflicts) {
  331.   // Find the first open row on the Report tab below the headers and create a
  332.   // range large enough to hold all of the failures, one per row.
  333.   var lastRow = spreadsheet.getSheetByName('Conflicts')
  334.     .getDataRange().getLastRow();
  335.   var headers = spreadsheet.getRangeByName('Headers');
  336.   var outputRange = headers
  337.     .offset(lastRow - headers.getRow() + 1, 0, conflicts.length);
  338.  
  339.   // Build each row of output values in the order of the columns.
  340.   var outputValues = [];
  341.   for (var i = 0; i < conflicts.length; i++) {
  342.     var conflict = conflicts[i];
  343.     outputValues.push([
  344.       customerId,
  345.       conflict.negative,
  346.       conflict.level,
  347.       conflict.positives.join(', '),
  348.       conflict.campaignName,
  349.       conflict.adGroupName,
  350.       conflict.listName
  351.     ]);
  352.   }
  353.   outputRange.setValues(outputValues);
  354.  
  355.   spreadsheet.getRangeByName('RunDate').setValue(new Date());
  356.  
  357.   for (var i = 0; i < CONFIG.RECIPIENT_EMAILS.length; i++) {
  358.     spreadsheet.addEditor(CONFIG.RECIPIENT_EMAILS[i]);
  359.   }
  360. }
  361.  
  362. /**
  363.  * Sends an email to a list of email addresses with a link to the spreadsheet.
  364.  *
  365.  * @param {Object} spreadsheet The spreadsheet object.
  366.  */
  367. function sendEmail(spreadsheet) {
  368.   MailApp.sendEmail(CONFIG.RECIPIENT_EMAILS.join(','),
  369.       'Negative Keyword Conflicts Found',
  370.       'Negative keyword conflicts were found in your ' +
  371.       'Google Ads account(s). See ' +
  372.       spreadsheet.getUrl() + ' for details. You may wish ' +
  373.       'to delete the negative keywords causing the ' +
  374.       'the conflicts.');
  375. }
  376.  
  377. /**
  378.  * Retrieves the campaign IDs of a campaign iterator.
  379.  *
  380.  * @param {Object} campaigns A CampaignIterator object.
  381.  * @return {Array.<Integer>} An array of campaign IDs.
  382.  */
  383. function getCampaignIds(campaigns) {
  384.   var campaignIds = [];
  385.   while (campaigns.hasNext()) {
  386.     campaignIds.push(campaigns.next().getId());
  387.   }
  388.  
  389.   return campaignIds;
  390. }
  391.  
  392. /**
  393.  * Retrieves all campaign IDs in an account.
  394.  *
  395.  * @return {Array.<Integer>} An array of campaign IDs.
  396.  */
  397. function getAllCampaignIds() {
  398.   return getCampaignIds(AdsApp.campaigns().get());
  399. }
  400.  
  401. /**
  402.  * Retrieves the campaign IDs with a given label.
  403.  *
  404.  * @param {string} labelText The text of the label.
  405.  * @return {Array.<Integer>} An array of campaign IDs, or null if the
  406.  *     label was not found.
  407.  */
  408. function getCampaignIdsWithLabel(labelText) {
  409.   var labels = AdsApp.labels()
  410.     .withCondition('Name = "' + labelText + '"')
  411.     .get();
  412.  
  413.   if (!labels.hasNext()) {
  414.     return null;
  415.   }
  416.   var label = labels.next();
  417.  
  418.   return getCampaignIds(label.campaigns().get());
  419. }
  420.  
  421. /**
  422.  * Compares a set of negative keywords and positive keywords to identify
  423.  * conflicts where a negative keyword blocks a positive keyword.
  424.  *
  425.  * @param {Array.<Object>} negatives A list of objects with fields
  426.  *     display, raw, and matchType.
  427.  * @param {Array.<Object>} positives A list of objects with fields
  428.  *     display, raw, and matchType.
  429.  * @return {Array.<Object>} An array of conflicts, each an object with
  430.  *     the negative keyword display text causing the conflict and an array
  431.  *     of blocked positive keyword display texts.
  432.  */
  433. function checkForConflicts(negatives, positives) {
  434.   var conflicts = [];
  435.  
  436.   for (var i = 0; i < negatives.length; i++) {
  437.     var negative = negatives[i];
  438.     var anyBlock = false;
  439.     var blockedPositives = [];
  440.  
  441.     for (var j = 0; j < positives.length; j++) {
  442.       var positive = positives[j];
  443.  
  444.       if (negativeBlocksPositive(negative, positive)) {
  445.         anyBlock = true;
  446.         blockedPositives.push(positive.display);
  447.       }
  448.     }
  449.  
  450.     if (anyBlock) {
  451.       conflicts.push({
  452.         negative: negative.display,
  453.         positives: blockedPositives
  454.       });
  455.     }
  456.   }
  457.  
  458.   return conflicts;
  459. }
  460.  
  461. /**
  462.  * Removes leading and trailing match type punctuation from the first and
  463.  * last character of a keyword's text, if any.
  464.  *
  465.  * @param {string} text A keyword's text to remove punctuation from.
  466.  * @param {string} open The character that may be the first character.
  467.  * @param {string} close The character that may be the last character.
  468.  * @return {Object} The same text, trimmed of open and close if present.
  469.  */
  470. function trimKeyword(text, open, close) {
  471.   if (text.substring(0, 1) == open &&
  472.       text.substring(text.length - 1) == close) {
  473.     return text.substring(1, text.length - 1);
  474.   }
  475.  
  476.   return text;
  477. }
  478.  
  479. /**
  480.  * Normalizes a keyword by returning a raw and display version and consistent
  481.  * match type. The raw version has no leading and trailing punctuation for
  482.  * phrase and exact match keywords, no consecutive whitespace, is all
  483.  * lowercase, and removes broad match qualifiers. The display version has no
  484.  * consecutive whitespace and is all lowercase. The match type is uppercase.
  485.  *
  486.  * @param {string} text A keyword's text that should be normalized.
  487.  * @param {string} matchType The keyword's match type.
  488.  * @return {Object} An object with fields display, raw, and matchType.
  489.  */
  490. function normalizeKeyword(text, matchType) {
  491.   var display;
  492.   var raw = text;
  493.   matchType = matchType.toUpperCase();
  494.  
  495.   // Replace leading and trailing "" for phrase match keywords and [] for
  496.   // exact match keywords, if it is there.
  497.   if (matchType == 'PHRASE') {
  498.     raw = trimKeyword(raw, '"', '"');
  499.   } else if (matchType == 'EXACT') {
  500.     raw = trimKeyword(raw, '[', ']');
  501.   }
  502.  
  503.   // Collapse any runs of whitespace into single spaces.
  504.   raw = raw.replace(new RegExp('\\s+', 'g'), ' ');
  505.  
  506.   // Keywords are not case sensitive.
  507.   raw = raw.toLowerCase();
  508.  
  509.   // Set display version.
  510.   display = raw;
  511.   if (matchType == 'PHRASE') {
  512.     display = '"' + display + '"';
  513.   } else if (matchType == 'EXACT') {
  514.     display = '[' + display + ']';
  515.   }
  516.  
  517.   // Remove broad match modifier '+' sign.
  518.   raw = raw.replace(new RegExp('\\s\\+', 'g'), ' ');
  519.  
  520.   return {display: display, raw: raw, matchType: matchType};
  521. }
  522.  
  523. /**
  524.  * Tests whether all of the tokens in one keyword's raw text appear in
  525.  * the tokens of a second keyword's text.
  526.  *
  527.  * @param {string} keywordText1 the raw keyword text whose tokens may
  528.  *     appear in the other keyword text.
  529.  * @param {string} keywordText2 the raw keyword text which may contain
  530.  *     the tokens of the other keyword.
  531.  * @return {boolean} Whether all tokens in keywordText1 appear among
  532.  *     the tokens of keywordText2.
  533.  */
  534. function hasAllTokens(keywordText1, keywordText2) {
  535.   var keywordTokens1 = keywordText1.split(' ');
  536.   var keywordTokens2 = keywordText2.split(' ');
  537.  
  538.   for (var i = 0; i < keywordTokens1.length; i++) {
  539.     if (keywordTokens2.indexOf(keywordTokens1[i]) == -1) {
  540.       return false;
  541.     }
  542.   }
  543.  
  544.   return true;
  545. }
  546.  
  547. /**
  548.  * Tests whether all of the tokens in one keyword's raw text appear in
  549.  * order in the tokens of a second keyword's text.
  550.  *
  551.  * @param {string} keywordText1 the raw keyword text whose tokens may
  552.  *     appear in the other keyword text.
  553.  * @param {string} keywordText2 the raw keyword text which may contain
  554.  *     the tokens of the other keyword in order.
  555.  * @return {boolean} Whether all tokens in keywordText1 appear in order
  556.  *     among the tokens of keywordText2.
  557.  */
  558. function isSubsequence(keywordText1, keywordText2) {
  559.   return (' ' + keywordText2 + ' ').indexOf(' ' + keywordText1 + ' ') >= 0;
  560. }
  561.  
  562. /**
  563.  * Tests whether a negative keyword blocks a positive keyword, taking into
  564.  * account their match types.
  565.  *
  566.  * @param {Object} negative An object with fields raw and matchType.
  567.  * @param {Object} positive An object with fields raw and matchType.
  568.  * @return {boolean} Whether the negative keyword blocks the positive keyword.
  569.  */
  570. function negativeBlocksPositive(negative, positive) {
  571.   var isNegativeStricter;
  572.  
  573.   switch (positive.matchType) {
  574.     case 'BROAD':
  575.       isNegativeStricter = negative.matchType != 'BROAD';
  576.       break;
  577.  
  578.     case 'PHRASE':
  579.       isNegativeStricter = negative.matchType == 'EXACT';
  580.       break;
  581.  
  582.     case 'EXACT':
  583.       isNegativeStricter = false;
  584.       break;
  585.   }
  586.  
  587.   if (isNegativeStricter) {
  588.     return false;
  589.   }
  590.  
  591.   switch (negative.matchType) {
  592.     case 'BROAD':
  593.       return hasAllTokens(negative.raw, positive.raw);
  594.       break;
  595.  
  596.     case 'PHRASE':
  597.       return isSubsequence(negative.raw, positive.raw);
  598.       break;
  599.  
  600.     case 'EXACT':
  601.       return positive.raw === negative.raw;
  602.       break;
  603.   }
  604. }
  605.  
  606. /**
  607.  * Validates the provided spreadsheet URL to make sure that it's set up
  608.  * properly. Throws a descriptive error message if validation fails.
  609.  *
  610.  * @param {string} spreadsheeturl The URL of the spreadsheet to open.
  611.  * @return {Spreadsheet} The spreadsheet object itself, fetched from the URL.
  612.  * @throws {Error} If the spreadsheet URL hasn't been set
  613.  */
  614. function validateAndGetSpreadsheet(spreadsheeturl) {
  615.   if (spreadsheeturl == 'YOUR_SPREADSHEET_URL') {
  616.     throw new Error('Please specify a valid Spreadsheet URL. You can find' +
  617.         ' a link to a template in the associated guide for this script.');
  618.   }
  619.   return SpreadsheetApp.openByUrl(spreadsheeturl);
  620. }
  621.  
  622. /**
  623.  * Validates the provided email address to make sure it's not the default.
  624.  * Throws a descriptive error message if validation fails.
  625.  *
  626.  * @throws {Error} If the list of email addresses is still the default
  627.  */
  628. function validateEmailAddresses() {
  629.   if (CONFIG.RECIPIENT_EMAILS &&
  630.       CONFIG.RECIPIENT_EMAILS[0] == 'YOUR_EMAIL_HERE') {
  631.     throw new Error('Please either specify a valid email address or clear' +
  632.         ' the RECIPIENT_EMAILS field.');
  633.   }
  634. }
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top