2
votes

Overview: the workbook calculates an Overall Score for each Record (or row) based on the Category Scores for each Record. Category Scores are derived from computations performed on hundreds of data points for each Record. I compiled code to send an email onChange called checkComplete (below) based on several examples and tutorials I found online. checkComplete would only send an email if the status of the Record changed to "Complete." An onChange trigger is required since the workbook uses Importrange to load the raw data, and no human interaction takes place for an onEdit trigger.

The problem began when I noticed an email would be sent upon any change being made, not just the status change in column O from a blank cell to "Complete". But the code worked well enough until I made significant edits to the Google Sheets workbook that included:

  1. Modifying computational formulas to display a set number of decimals

    e.g. =if(round('2-Comp'!F14,4)=0,"",round('2-Comp'!F14,4))

  2. Moving columns around

    e.g. switching columns N and O, where N was the original column checked by the onChange trigger

  3. Updating source data, which therefore updated scores.

    e.g. a Record's data point changed from $52,420 to $54,323

Then the email bombardment began. The net result is that the script began to send an email for every Record starting with the first record, and would generate the following error messages in succession:

Service invoked too many times for one day: email. (line 47, file "checkComplete")

Exceeded maximum execution time

Service using too much computer time for one day

I have since commented out MailApp.sendEmail() to cease blowing up my inbox, but the script continues to generate the "Exceeded maximum execution time" and "Service using too much computer time for one day". My thought was that the script was working off all the changes described above, but now that this problem has persisted for over a week I decided to seek out help.

I thought the issue is with the array, specifically var oldValues, so I tried:

  1. to call flush() function to clear out the array, which did not solve the issue.

  2. to empty the array as per this thread - How do I empty an array in JavaScript?, but that did not resolve the issue either.

I now think the problem may be related to the logger, but to be honest I am stumped.

Below is the script's source code in its entirety:

checkComplete.gs

var admin_email='[email protected]'; //<- list of email recipients goes here

function checkComplete() {
  var sh = SpreadsheetApp.getActiveSheet();
  var values = sh.getRange('O14:O').getValues().join('-');
  if(PropertiesService.getScriptProperties().getKeys().length==0){ // first time you run the script
  PropertiesService.getScriptProperties().setProperty('oldValues', values);
  return;
}
  var oldValues = PropertiesService.getScriptProperties().getProperty('oldValues').split('-');
  var valuesArray = values.split('-');
  while (valuesArray.length>oldValues.length){
    oldValues.push('x'); // if you append some rows since last exec
  }
  Logger.log('oldValues = '+oldValues)
  Logger.log('current values = '+valuesArray)
  for(var n=0;n<valuesArray.length;n++){
    if(oldValues[n] != valuesArray[n]){ // check for any difference
      sendMail(n+1,valuesArray[n]);
    }
  }
   PropertiesService.getScriptProperties().setProperty('oldValues', values);

}
function sendMail(row,val){
  Logger.log('value changed on row '+row+' value = '+val+' ,  mail sent');
  var rank = SpreadsheetApp.getActiveSheet().getRange(row,1).getValue();
// other var that I deleted here to shorten this post
// MailApp.sendEmail(admin_email,'message that calls vars');

}

I expect to receive an email only upon the change of a Record's status, indicated in column O, from a blank cell to "Complete". All other data updates should not trigger any email. Any advice/help is appreciated. Thank you for taking the time to review my problem.

1
GMT, did you figure this out? Copying your code worked for me with no problems.jdavis
Can I see your spreadsheet?ziganotschka
@JonathanDavis, that is encouraging. No, the error log continues to grow each day. I wonder what the issue is. I am going to post a link to view the spreadsheet in a reply to zianotschka in case you were interested in taking a look.GMT Skunkworks
@ziganotschka, sure, I just opened the spreadsheet up for viewing here docs.google.com/spreadsheets/d/…. FYI, I continue to receive an error log that contains the following two message: "Exceeded maximum execution time" and "Service using too much computer time for one day" while MailApp.sendEmail() is commented out.GMT Skunkworks
@GMTSkunkworks did you set up an installable trigger? I'm assuming so but if so, did you use onChange or onEdit? I'd check using the menu>current project triggers to ensure that you haven't added a bunch of triggers for the same function (which could explain the service invoked too many times for one day error). If so, you can use a script to remove them all quickly. I'll provide the code if that's the issuejdavis

1 Answers

0
votes

UPDATE

After specifying that the entries in Column O will be made by a human and will fire an onEdit() trigger, here is an easy and elegant solution making use of event objects:

 onEdit(e){
  if( e.range.getSheet().getName()=='Sheet1' &&
    e.range.getColumn()==15&&e.value=="Complete"&&e.oldValue!="Complete"){
    sendMail(e.range.getRow(),e.value);
   }  
}  

You can replace function checkComplete() and its onChange trigger by this onEdit function.


After reviewing your spreadsheet I noticed following issues:

  1. As mentioned in my comment, onEdit will be triggered by a change in any of the sheets within the spreadsheet, but you want the code to run only for changes in the sheet "Dashboard". And also, you do not want to compare the column O in Dashboard against the columns O in the other sheets - where all entries will be different and would trigger the call of sendMail. You should implement a condition statement to verify at the very beginning of "Dashboard that you are in the right sheet, before continuing with the rest of the code.

  2. Your function sendMail contains

  var rank = SpreadsheetApp.getActiveSheet().getRange(row,1).getValue();
  var totalranked = SpreadsheetApp.getActiveSheet().getRange(4,1).getValue();
  var address = SpreadsheetApp.getActiveSheet().getRange(row,2).getValue();
  var score = SpreadsheetApp.getActiveSheet().getRange(row,6).getValue();
  var outlook = SpreadsheetApp.getActiveSheet().getRange(row,14).getValue();
  var ildemand = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('1-Dmnd').getRange(row+1,9).getValue();
  var aldemand = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('1-Dmnd').getRange(row+1,14).getValue();
  var mcdemand = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('1-Dmnd').getRange(row+1,19).getValue();
  var increase = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('2-Comp').getRange(row,11).getValue();
  var occupancy = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('2-Comp').getRange(row,13).getValue();
  var medHHI = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('4-Demo').getRange(row+2,17).getValue();
  var medHV = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('4-Demo').getRange(row+2,19).getValue();
  var walkscore = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('3-Muni').getRange(row,19).getValue();
  var dmnd = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('1-Dmnd').getRange(row+1,5).getValue()
  var comp = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('2-Comp').getRange(row,5).getValue();
  var muni = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('3-Muni').getRange(row,5).getValue();
  var grade = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('3-Muni').getRange(row,17).getValue();
  var demo  = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('4-Demo').getRange(row+2,5).getValue();

This is 18 calls to SpreadsheetApp for each call of sendMail. And this multiplied by the amount of time sendMail will be called if you compare columns O of two different sheets against each other (see 1.)! Be aware that any requests to external services are very time consuming and inefficient and should be avoided, see Best Practices. You should retrieve all those variables only once, outside of sendMail. And given that after this the code of sendMail will be short, you can implement it directly within your for loop.

  1. If a row is appended in the beginning rather than the end of the sheet - this will shift the new values against the old values and thus, each row will trigger the call of sendMail. You can avoid but checking either the new contents are contained within the array with the old contents with indexOf.

  2. Go from the Apps Script editor UI to Edit->Current project's triggers and check if you accidentally attached multiple triggers to the file, which all run simultaneously.

Taking in consideration the points 1., 2. and 3., here is an improvement suggestion for your code:


var admin_email=XXXXX; //<- list email addresses here

function checkComplete() {
  var ss=SpreadsheetApp.getActive();
  var sh = ss.getActiveSheet();
  if(sh.getName()=="Dashboard"){
   var values = sh.getRange('O14:O').getValues().join('-');
   if(PropertiesService.getScriptProperties().getKeys().length==0){ // first time you run the script
    PropertiesService.getScriptProperties().setProperty('oldValues', values);
    return;
   }
   var oldValues = PropertiesService.getScriptProperties().getProperty('oldValues').split('-');
   var valuesArray = values.split('-');
   while (valuesArray.length>oldValues.length){
    oldValues.push('x'); // if you append some rows since last exec
   }
   Logger.log('oldValues = '+oldValues)
   Logger.log('current values = '+valuesArray)
   for(var n=0;n<valuesArray.length;n++){
    if(oldValues.indexOf(valuesArray[n])==-1){ // check for any difference
        Logger.log('value changed on row '+n+1+' value = '+valuesArray[n]+' ,  mail sent');
      sendMail(n+1,valuesArray[n]);
    }
   }  
   PropertiesService.getScriptProperties().setProperty('oldValues', values);
  }   
}


function sendMail(row,val){

   Logger.log('value changed on row '+row+' value = '+val+' ,  mail sent');

   var rank = sh.getRange(row,1).getValue();
   var totalranked = sh.getRange(4,1).getValue();
   var address = sh.getRange(row,2).getValue();
   var score = sh.getRange(row,6).getValue();
   var outlook = sh.getRange(row,14).getValue();
   var ildemand = ss.getSheetByName('1-Dmnd').getRange(row+1,9).getValue();
   var aldemand = ss.getSheetByName('1-Dmnd').getRange(row+1,14).getValue();
   var mcdemand = ss.getSheetByName('1-Dmnd').getRange(row+1,19).getValue();
   var increase = ss.getSheetByName('2-Comp').getRange(row,11).getValue();
   var occupancy = ss.getSheetByName('2-Comp').getRange(row,13).getValue();
   var medHHI = ss.getSheetByName('4-Demo').getRange(row+2,17).getValue();
   var medHV = ss.getSheetByName('4-Demo').getRange(row+2,19).getValue();
   var walkscore = ss.getSheetByName('3-Muni').getRange(row,19).getValue();
   var dmnd = ss.getSheetByName('1-Dmnd').getRange(row+1,5).getValue()
   var comp = ss.getSheetByName('2-Comp').getRange(row,5).getValue();
   var muni = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('3-Muni').getRange(row,5).getValue();
   var grade = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('3-Muni').getRange(row,17).getValue();
   var demo  = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('4-Demo').getRange(row+2,5).getValue();


      // MailApp.sendEmail(admin_email,'EARLY results for '+address+' are ready!','EARLY says the Site is '+outlook+' and ranks # '+rank+' out of '+totalranked+' with an overall Score of '+score+'. The projected Net Demand is '+dmnd+' with: IL '+ildemand+' units, AL '+aldemand+' units and MC '+mcdemand+' units. The competitive environment is '+comp+' with an occupancy rate of '+occupancy*100+'% for nearby properties, and an expected supply increase of '+increase*100+'%. The location is '+walkscore+', and its public schools have a grade of '+grade+' making it '+muni+'. The demographic trends are '+demo+' with Median HHI of $'+medHHI+' and a median Home Value of $'+medHV+'. For more information, please visit https://docs.google.com/spreadsheets/d/1ydcXQilx6hxhI6HPpWTPT7Bq9a-gXPa7h8UC6KL9W8c/edit?usp=sharing.');
    // add senior growth rate(s) to the Demographic summary sentence - Sept 3 2019
}