Inconsistent error message ''Action not allowed (line 298, file "<functionName>")" - google-apps-script

Context
Using g-suite, I have a google form that users submit information to a google sheet. Code aligned to the google sheet takes the submitted information and outputs a google doc/pdf. This final doc/pdf is created by copying a master doc template, merging/appending sections from other template docs, and finally search and replacing a number of text-merge fields in the final doc. The part of the code that is failing is a section that is responsible from merging/appending sections from other template docs as referenced in the sample code.
The code is run from a 'trigger'. The trigger is configured as:
Select event source : From spreadsheet
Select event type: on form submit
Issue
Without making any code changes, recently the code has stopped working when run from the trigger, error-ing out with the:
Action not allowed (line 298, file "")
The actual line of code at 298 is:
'TargetDocBody.appendParagraph(element);'
I can run the function in question successfully in the GAS script editor by selecting Run -> Run Function -> . The function does NOT run when configured as a trigger. Normally I would chalk this up to a permission issue, however even I, the script owner/writer, cannot get the function to run when I am the submitter of the form. It does run successfully as mentioned above within the GAS editor.
I've given full write permissions to all template dates, directories where the files are ultimately saved, write permission to the google form, write permission to the google sheets etc etc and I still can't seem to get around 'Action not allowed' when run from a trigger. I've logged the active and effective users on failures and successes and they're the same output both times.
[19-04-24 09:52:28:837 EDT] Active user : tim_monaco#company.com
[19-04-24 09:52:28:839 EDT] EffectiveUser : tim_monaco#company.com
function MergeSections(ServiceTypeDoc,TargetDocBody)
{
var ServiceTypeDocNumElements = ServiceTypeDoc.getNumChildren();
for(var x =0; x < ServiceTypeDocNumElements; ++x)
{
var element = ServiceTypeDoc.getChild(x).copy();
var type = element.getType();
if(type == DocumentApp.ElementType.PARAGRAPH) //this is the problem line
TargetDocBody.appendParagraph(element);
if(type == DocumentApp.ElementType.LIST_ITEM)
TargetDocBody.appendListItem(element);
if(type == DocumentApp.ElementType.FOOTER_SECTION)
TargetDocBody.appendFooterSection(element);
}
}
Function should/has run without issues in the past. With no code changes, I would expect the same.
Actual results are a failure and stop mid execution with
Action not allowed (line 298, file "")
The actual line of code at 298 is:
'TargetDocBody.appendParagraph(element);'

This could be a situation where the error message is inaccurate. It often happens that quota limit errors don't happen initially, and then mysteriously kick in. Also, because it happens inconsistently, that would also point to a quota limit error. This could be a situation where your for loop is calling a service too often in a short period of time, and hitting a quota limit. I could be wrong, but I'd try putting in a try / catch and if there is an error, sleep the code and try again.
If the following code that sleeps when there is an error, doesn't work at all, then I'd consider that it might be a bug, and submit an issue.
function MergeSections(ServiceTypeDoc,TargetDocBody)
{
var ServiceTypeDocNumElements = ServiceTypeDoc.getNumChildren();
for(var x =0; x < ServiceTypeDocNumElements; ++x)
{
var element = ServiceTypeDoc.getChild(x).copy();
var type = element.getType();
for (var i2 = 0;i2<3;i2++) {//Try 3 times
try{
switch(true) {
case type == DocumentApp.ElementType.PARAGRAPH:
TargetDocBody.appendParagraph(element);
break;
case type == DocumentApp.ElementType.LIST_ITEM:
TargetDocBody.appendListItem(element);
break;
case type == DocumentApp.ElementType.FOOTER_SECTION:
TargetDocBody.appendFooterSection(element);
break;
}
break;//If there is no error then break the loop
}catch(e) {
Utilities.sleep(1500);//Wait x number of seconds
}
}
}
}

Related

GmailThread.getlables() fails randomly with the exception "Exception: Gmail operation not allowed."

This might be a weird one but I have some code that goes through my email and uses labels to trigger certain tasks. For example, if someone applies via Indeed, Indeed sends me an email. A filter will see this this email and apply the "Applications/indeedApplication" label. Then, a script that runs once per minute will find this label, and based on the label, perform some actions automatically.
This works perfectly about 99.97% of the time. The script runs with no issues and does the tasks, applies a completed label, and everything works as intended. 0.03% of the time, the task will fail, always at the same point, and always with the same exception:
Exception: Gmail operation not allowed.
It fails when the scrip tries to run the getLables() function on a GmmailThread (class of GmailApp API).
Code in question:
function addICandidate() {
var label = GmailApp.getUserLabelByName("Applications/indeedApplication"); //Defines the label that identifies indeed applicatoins
var requests = label.getThreads(); //Returns an array of threads which have this label
if(requests.length > 3){requests.length = 3}; //This makes it so the script doesn't waste time looking through every application ever.
for ( var i in requests ) { //Starting a normal for loop
iLabels = requests[i].getLabels(); //Gets the labels of the Randomly fails here and only here
var checker = false; //used later to check if tasks were already completed
for (var k = 0 in iLabels) { //Starting another normal for loop
var individualLabel = iLabels[k].getName(); //Gets a list of the labels
if (individualLabel === completedLabelName) { //Checks if the label for completes tasks is present
var checker = true; //If they are, set checker to true (also used later)
}
}
if(checker){ //If the checker above is set to true
Logger.log("true"); //Do nothing (logger is just used in debugging)
}
else{ //Else (if the label is not completed and therefore, the tasks have not been done yet)
var thread = requests[i].Dostuff(); //Confidential stuff lives here
}
requests[i].addLabel(completedlabel); //After everything, apply the completed label so tasks are not done again on the next pass
}
}
}
Does anyone know why it would fail here and only 1 in 3000 attempts. I obviously cannot debug this with brute force so any ideas as to why this might happen would be really helpful.

"We're sorry, a server error occurred. Please wait a bit and try again" while creating charts

We are running into sporadic errors (more often than not) on a project that generates Google Doc documents based on info entered into Google Sheet spreadsheets.
The Google Apps Script project pulls data (from sheets), caches it in-memory with standard var statements, massages the data a bit (formatting, de-duping, etc), then copies a Doc template and does a bunch of token substitution and also inserts some charts based on the in-memory variables.
We are encountering the following error during a high quantity of executions:
Exception: We're sorry, a server error occurred. Please wait a bit and try again.
at insertChart(Code:5890:10)
at processForm(Code:4540:5)
The main method creates several (between 5 - 20 or more) charts depending on parameters entered at run-time by the user (which happens via a standard Web Form created by GAS:
function doGet() {
return HtmlService
.createTemplateFromFile('index')
.evaluate();
}
The web page returned by the above is a very standard GAS HTMLService webapp. It contains a form that allows a user to select a few standard types of reporting criteria (date range, filter by certain types of data, etc).
We are duplicating a "template" Docs file which has a bunch of common text and some tokens that we use as placeholders to find and replace with data loaded from the Sheets. The Sheets are very common spreadsheets with the kind of data you'd imagine: date ranges, selections from drop-down lists, free-text columns, etc. (The filtering info specified by the user is used to query the Sheets for the data we want to "merge" into the new Doc. It's basically a much more complex version of your standard "mail merge".)
var templateId = 'asdfasdfasdfasdfasdfasdfasdfasdfasdf'; // DEV
var documentId = DriveApp.getFileById(templateId).makeCopy().getId();
DriveApp.getFileById(documentId).setName('Demo Report - Project Report');
DriveApp.getFileById(documentId).setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.COMMENT);
Logger.log('templateId: ' + templateId);
Logger.log('documentId: ' + documentId);
var doc = DocumentApp.openById(documentId);
var body = doc.getBody();
We have the following method which takes the Doc body, placeholder, and formatted data from a chart builder. It then identifies the Doc element token to replace (simply a string in the Doc file), and runs a body.insertImage after that token element.
function insertChart(body, placeholder, chart) {
if (placeholder.getParent().getType() === DocumentApp.ElementType.BODY_SECTION && typeof chart !== 'undefined' && chart !== null) {
var offset = body.getChildIndex(placeholder);
//Logger.log(chart);
body.insertImage(offset + 1, chart);
}
}
We have several helper methods like the following buildStackedColumnChart(). It's meant to wrap the Chart API commands to use a dataTable and Chart Builder to return a specific type of chart (bar, stacked bar, line, etc).
Its resulting chart gets passed into the insertChart() method above.
function buildStackedColumnChart(title, header, data) {
Logger.log('buildStackedColumnChart');
Logger.log('title: ' + title);
Logger.log('header: ' + header);
Logger.log('data: ' + data);
try {
var dataTable = Charts.newDataTable();
for (var hr = 0; hr < header.length; hr++) {
var headerRow = header[hr];
dataTable.addColumn(headerRow[0], headerRow[1]);
}
for (var r = 0; r < data.length; r++) {
var row = data[r];
dataTable.addRow(row);
}
dataTable.build();
var chart = Charts.newColumnChart()
.setDataTable(dataTable)
.setDimensions(600, 500)
.setTitle(title)
.setStacked()
.build();
return chart;
} catch (ex) {
Logger.log('ex: ' + ex);
return null;
}
}
In the main processForm() method (called directly by the webapp menioned above when the user selects their criteria and clicks a "Generate Report" button. We have a few calls to a method which finds the token in the template file (just text with {{}} around it as shown below). It iterates through a hard-coded list of values in the in-memory data variables storing accumulations from the Sheets and creates chartHeader and chartData (which contains the values to be charted in a mechanism the helper methods above can translate back into calls that make sense for the Chart API) and uses the insertChart() and `` helper methods to insert the chart after the bookmark token and then remove the bookmark from the document (cleanup so the tokens aren't present in the end report Doc).
var chartTrainingsSummaryBookmark = findPlaceholder(body, '{{CHART_TRAININGS_SUMMARY}}');
Logger.log('chartTrainingsSummaryBookmark: ' + chartTrainingsSummaryBookmark);
var chartCategories = [
'Call',
'Conference Attendee',
'Meeting (External)',
'Partnership',
'Quarterly Board Meeting',
'Alliance or Workgroup',
'Training (Others)',
'Training (Professional Development)',
'Youth Training',
'Webinar or Zoom',
'Other',
'Coalition',
'Email',
'Face-to-face',
'Phone',
'Site Visit',
];
// {Call=10.0, Conference Attendee=10.0, Quarterly Board Meeting=10.0, Alliance or Workgroup=10.0, Coalition=10.0, Meeting (External)=10.0}
var chartHeaderData = [];
var chartData = [];
var monthChartHeader = [[Charts.ColumnType.STRING, 'Month']];
var monthChartData = [monthName];
for (var cci = 0; cci < chartCategories.length; cci++) {
var chartCategory = monthName + ':' + chartCategories[cci];
if (getChartData(chartCategory, trainingsChartData) > 0) {
monthChartHeader.push([Charts.ColumnType.NUMBER, chartCategory.split(':')[1]]);
monthChartData.push(getChartData(chartCategory, trainingsChartData));
}
}
if (monthChartData.length > 1) {
if (chartHeaderData.length < 1) {
chartHeaderData = chartHeaderData.concat(monthChartHeader);
}
chartData.push(monthChartData);
}
Logger.log('----- CHART - TRAININGS: SUMMARY -----');
if (chartData.length > 0 && chartData[0].length > 1) {
insertChart(body, chartTrainingsSummaryBookmark, buildStackedColumnChart('', chartHeaderData, chartData));
}
chartTrainingsSummaryBookmark.removeFromParent();
We are also seeing slight variations that also error out the document generation with a Timeout. (This process can take a while if there is a lot of data that fits within the parameters which the user specified.)
We were wondering why these errors would happen on a seemingly random interval. We can literally just click the button that makes the call again and it might work, or might error out on a different chart, etc.
This script has been working for the most part (we did encounter a few specific errors) in months previous; however, this "Exception: We're sorry, a server error occurred. Please wait a bit and try again." started occuring last weekend and got far worse on Monday / Tuesday (nearly every execution failed, very few succeeded). It's not happening quite as badly today; but we are seeing a few errors.
Also, it's a bit strange, but copying the GAS Project and executing the copy has thrown the error less frequently than the original project in production.
We have some ideas for workarounds; bu, ideally, we would like to identify a root cause so we can correctly fix the issue.

Why this error on line 3, file "Code" for onEdit(e)?

In Google Sheets, I am wanting to do something like the following:
If B129=I3, then D129 = 1. 1 now is to be held and never forgotten.
Then, if B129=I3 again, then D129 = 2. D129 can now repetitively increase by 1 over and over again as many times as B129=I3.
Thanks to a wonderful member here, I now have a framework for how this can possibly be done. But I am getting an error message that says: TypeError: Cannot read property "range" from undefined. (line 3, file "Code"), using the code down below?
Because of this error, the problem that I am having is that the code does not run on the spreadsheet at all. Nothing happens, even if I were to perform what I need to do on the actual spreadsheet, because the error continues to come up as: Cannot read property "range" from undefined. (Line 3, file code).
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("M Numbers");
if (e.range.getA1Notation() != 'B129') return; //Something other than the cell you wanted changed
var x = ss.getRange("I3").getValue();
if (e.getValue() != x) return; //B129 doesn't equal I3
var counter = ss.getRange("D129"); // cell reference
counter.setValue(counter.getValue() + 1);
}
This onEdit(e) script is using Event Objects. You can't run these scripts manually, they run automatically when the spreadsheet is edited.
e.range is where you're trying to call the event object to get the edited range of the sheet. Since there's no edited range to pull when you run it manually in script editor, it'll fail with the error you're experiencing.

Google Spreadsheets: Script to check for completion of ImportHTML

I am trying to scrape data of a website once day automatically. In Google Spreadsheets, i use the =ImportHTML() function to import data tables, and then I extract the relevant data with a =query(). These functions take between 10 and 30 seconds to complete calculation, every time I open the spreadsheet.
I use a scheduled Google Apps Script, to copy the data into a different sheet (where it is stored, so i can run statistics) every day.
My problem is that I am having trouble to make the script wait for the calculations to be finished, before the data is copied. The Result is that my script just copies the error Message "N/A".
I tried just adding a Utilities.sleep(60000);, but it didn't work.
Is it possible to create a loop, that checks for the calculation to finish? I tried this without success:
function checkForError() {
var spreadsheet = SpreadsheetApp.getActive();
var source = spreadsheet.getRange ("Today!A1");
if (source = "N/A") {
Utilities.sleep(6000);
checkForError();
} else {
moveValuesOnly();
}
}
Locks are for this. Look up lock services in the docs. Use a public lock.
Here's how I used Zig's suggestion (combined with my own check loop) to solve my similar problem:
// Get lock for public shared resourse
var lock = LockService.getPublicLock();
// Wait for up to 120 seconds for other processes to finish.
lock.waitLock(120000);
// Load my values below
// something like sheet.getRange("A1").setFormula('= etc...
// Now force script to wait until cell D55 set to false (0) before
// performing copy / pastes
var current = SpreadsheetApp.setActiveSheet(sheet.getSheets()[1]);
var ready = 1;
var count = 0;
while (true) {
// break out of function if D55 value has changed to zero or counter
// has hit 250
if (count >= 250) break;
// otherwise keep counting...
ready = current.getRange("D55").getValue();
if (ready == 0) {count = 400;}
Utilities.sleep(100);
++count;
}
// wait for spreadsheet to finish... sigh...
Utilities.sleep(200);
// Do my copy and pastes stuff here
// for example sheet.getRange("a1:b1").copyTo(sheet.getRange("a3"), {contentsOnly:true});
// Key cells are updated so release the lock so that other processes can continue.
lock.releaseLock();
// end script
return;
}
This has worked fantastic for me, stopped Google's sporadic service from ruining my work!
Thanks goes to Zig's suggestion!

Cells containing custom functions return "NaN'" when spreadsheet is not open. (Apps Script webapp accessing google spreadsheet)

As a high school teacher, I record all of my grading in a Google spreadsheet. I have written custom functions within that spreadsheet that are accessed in the spreadsheet. That all works fine.
I also have a simple (but independent) web app written in google apps script that summarizes the grade information for each student that accesses it and returns it in a table. This has operated perfectly for about 8 months. However, students now get a "NaN" error when trying to check their grades. The "NaN" is only returned for cells that use custom functions. By simply opening the source spreadsheet, it fixes the problem temporarily. But soon after closing the spreadsheet the webapp begins returning "NaN" again.
I'm assuming that it has something to do with when/how these cells are recalculated but I can't figure out how to make the cells retain their value while the spreadsheet is closed. Any help would be much appreciated.
With Eric's advice, I implemented the following function (which runs early on in my app):
function refreshSheet(spreadsheet, sheet) {
var dataArrayRange = sheet.getRange(1,1,sheet.getLastRow(),sheet.getLastColumn());
var dataArray = dataArrayRange.getValues(); // necessary to refresh custom functions
var nanFound = true;
while(nanFound) {
for(var i = 0; i < dataArray.length; i++) {
if(dataArray[i].indexOf('#N/A') >= 0) {
nanFound = true;
dataArray = dataArrayRange.getValues();
break;
} // end if
else if(i == dataArray.length - 1) nanFound = false;
} // end for
} // end while
}
It basically keeps refreshing the sheet (using .getValues()) until all of the #N/A's disappear. It works fabulously but does add a small lag.