Google Apps Script doPost() function runs multiple times - javascript

I have an independent script that receives a POST request and fills the request's body data into a specific Spreadsheet. The Spreadsheet name/ID depends on the body data itself.
Usually, it works fine. Someone triggers a webhook from a separate service, it reaches the script, the function gets executed, and a new row in a specific SS is appended. However, sometimes the function repeats its execution for couple times within a stated period of 2-3 seconds. It causes multiple rows to appear (2/3/4/5 same rows). Why is this happening?
The webhook itself triggers only once (I triple-checked it).
I'll attach the code and a screenshot illustrating the problem.
function doPost(request) {
var jsonString = request.postData.getDataAsString();
var jsonData = JSON.parse(jsonString);
var ticketId = jsonData.id;
var comment = jsonData.comment;
var locale = jsonData.locale;
var ssId = getSpreadsheetId(locale);
var sheetName = getSheetName(comment);
var updated = new Date();
var sheet = SpreadsheetApp.openById(ssId).getSheetByName(sheetName);
Logger.log(ticketId, userName, comment, locale, ssId, sheetName);
sheet.appendRow([ticketId, comment, "", "New", updated]);
if (sheet.getTabColor() == null) {
sheet.setTabColor("ff0000"); // Set the color to red.
}
}
// Search for a specific string and return the name of the sheet based on the result.
// #to translate in a message refers to "SEND" sheet in a Spreadsheet.
// #to understand in a message refers to "Understand" in a Spreadsheet.
function getSheetName(comment) {
if (comment.search("#to translate") >= 0) {
var sheetName = "SEND";
} else if (comment.search("#to understand") >= 0) {
var sheetName = "UNDERSTAND";
}
return sheetName;
}
// Return spreadsheet id based on locale that needs to be translated.
function getSpreadsheetId(locale) {
switch (locale) {
case "Deutsch":
var spreadSheetId = "qwerty1";
break;
case "简体中文 (Simplified Chinese)":
var spreadSheetId = "qwerty12";
break;
case "Italiano":
var spreadSheetId = "qwerty123";
break;
case "Français":
var spreadSheetId = "qwerty1234";
break;
case "Español":
var spreadSheetId = "qwerty12345";
break;
case "Português (Portugal)":
var spreadSheetId = "qwerty123456";
break;
case "繁體中文 (Traditional Chinese)":
var spreadSheetId = "qwerty1234567";
break;
case "日本語 (Japanese)":
var spreadSheetId = "qwerty12345678";
Logger.log("success");
break;
case "한국어 (Korean)":
var spreadSheetId = "qwerty123456789";
Logger.log("success");
break;
}
return spreadSheetId;
}
The exact same steps, as well as request body work fine at the moment.
When you create a custom function and define the data manually, it works fine too. One row is added as expected.
The issue happens randomly, sometimes (not always), leaving an 'upstream request timeout' response.

Related

Google Sheets Scripts - run scripts as administrator / owner

I have Google Sheet, name TEST https://docs.google.com/spreadsheets/d/1HsRwknyZBmZZ9nibDfNpOwqkVsFGThDyrTwspV-5_4U/edit?usp=sharing
Sheet: Arkusz 1
Column A: all people can edit
Column B: only owner can edit
Library (for everyone): https://script.google.com/macros/s/AKfycbzpnEMhIG-0dMp54q3W4UxoT71-lSdfF7Qxf7rq_j6gJMNIxuCS/exec
A user cannot add a row because it is blocked by column B, which belongs only to the admin.
How can I create macro, which allow user to add new rows?
I have three scripts:
function insertRow() {
var ss = SpreadsheetApp.getActive()
var sheetName = ss.getActiveSheet().getName()
var row = ss.getActiveRange().getRow()
var numRows = Browser.inputBox('Insert Rows', 'Enter the number of rows to insert', Browser.Buttons.OK);
Logger.log(numRows)
var url ="https://script.google.com/macros/s/AKfycbzpnEMhIG-0dMp54q3W4UxoT71-lSdfF7Qxf7rq_j6gJMNIxuCS/exec"
var queryString = "?sheetName="+sheetName+"&rowNo="+row+"&noOfRows="+numRows
url = url + queryString
Logger.log(url)
var request = UrlFetchApp.fetch(url)
if (request != 'Success')
Browser.msgBox(request)
}
Second:
function doGet(e) {
var param = e.queryString
var parameters = param.split("&")
// This just checks only 3 parameters are present else gives a invalid link
if (param != null && parameters.length == 3){
param = e.parameter
var name = param.sheetName
var row = Number(param.rowNo)
var numOfRows = Number(param.noOfRows)
} else{
return ContentService.createTextOutput("Invalid query")
}
try{
var ss = SpreadsheetApp.openById("https://docs.google.com/spreadsheets/d/1HsRwknyZBmZZ9nibDfNpOwqkVsFGThDyrTwspV-5_4U")
var sheet = ss.getSheetByName(name)
sheet.insertRowsAfter(row, numOfRows);
var source_range = sheet.getRange(row,1,1,sheet.getLastColumn());
var target_range = sheet.getRange(row+1,1,numOfRows);
source_range.copyTo(target_range);
}
catch (err){
return ContentService.createTextOutput("error: "+err)
}
return ContentService.createTextOutput("Success")
}
And after clicked function insertRow and filled number of rows I have doPost(e) information.
Could you help me?
On the solution you provided below, I see that the issue is in mainScript
function mainScript(e) {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
// assign the sheet to a variable and use it below instead of spreadsheet
var sheet = spreadsheet.getSheetByName('ZNC')
sheet.getRange('A2').activate()
sheet.insertRowsBefore(sheet.getActiveRange().getRow(), 1);
}
Hmm, I created solution, but I think there's a bug somewhere, because it doesn't add the line, even though everything is correct and the script is published as public.
function ZNCWiersz() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('ZNC'), true);
const activeSheet = SpreadsheetApp.getActiveSheet().getSheetName();
const url = ScriptApp.getService().getUrl();
UrlFetchApp.fetch(`${url}?sheetName=${activeSheet}`, {
headers: { authorization: "Bearer " + ScriptApp.getOAuthToken() },
});
// DriveApp.getFiles() // This is used for automatically detecting the scope of "https://www.googleapis.com/auth/drive.readonly". This scope is used for the access token.
}
// When runScript() is run, this function is run.
const doGet = (e) => ContentService.createTextOutput(mainScript(e));
// This script is run by Web Apps.
function mainScript(e) {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
spreadsheet.getSheetByName('ZNC')
spreadsheet.getRange('A2').activate()
spreadsheet.insertRowsBefore(spreadsheet.getActiveRange().getRow(), 1);
}

If GoogleJsonResponseException: then skip and move to next row

I have a working script. need to improvise to have no manual interruption. We have multiple Profiles in Analytics, sometimes we lose access and sometimes we have. So when i run the Script, If we lost access to 1 of 60 profiles, i have to delete that entry manually then rerun the script.
What i want is, If there is below error, Then skip and continue with next row
"GoogleJsonResponseException: API call to analytics.data.ga.get failed with error: User does not have sufficient permissions for this profile."
function GoogleAnalytics() {
var doc2 = SpreadsheetApp.getActiveSpreadsheet();
var dashboard = doc2.getSheetByName("Dashboard");
for(var i=52;i<65;i++){
var viewId = dashboard.getRange(i,13).getValue(); // Your Google Analytics view ID
var metric = 'ga:metric, ga:metric2, ga:metric3';
var option = {'segment': 'gaid::-5'};
var result = Analytics.Data.Ga.get(viewId, metric, option);
var metric = result.totalsForAllResults['ga:metric'];
var metric2 = result.totalsForAllResults['ga:metric2'];
var metric3 = result.totalsForAllResults['ga:metric3'];
var doc = SpreadsheetApp.getActiveSpreadsheet(); // Current document
var sheet = doc.getActiveSheet(); // Current sheet
sheet.getRange(i,14,1,1).setValue(metric);
sheet.getRange(i,15,1,1).setValue(metric2);
sheet.getRange(i,16,1,1).setValue(metric3);
} }
try it this way:
function GoogleAnalytics() {
var doc2 = SpreadsheetApp.getActiveSpreadsheet();
var sh = doc2.getSheetByName("Dashboard");
var sheet = doc2.getActiveSheet(); // Current sheet
const vs = sh.getRange(52, 13, 13).getValues();
var metric = 'ga:metric, ga:metric2, ga:metric3';
var option = { 'segment': 'gaid::-5' };
for (var i = 0; i < vs.length; i++) {
var viewId = vs[i][0]; // Your Google Analytics view ID
try {
var result = Analytics.Data.Ga.get(viewId, metric, option);
}
catch(e){
continue;
}
if (result) {
sheet.getRange(i + 52, 14, 1, 3).setValues([[result.totalsForAllResults['ga:metric'], result.totalsForAllResults['ga:metric2'], result.totalsForAllResults['ga:metric3']]]);
}
}
}
Without the benefit of working data some of this may not be correct but using setValues and getValues should speed it up considerably and the try catch blocks should help with not getting result consistently. Also you want to avoid making unnecessary declarations in loops.
I might understand the question incorrectly (if so, please clarify) but it sounds to me like you just need to add...
function GoogleAnalytics() {
var doc2 = SpreadsheetApp.getActiveSpreadsheet();
var dashboard = doc2.getSheetByName("Dashboard");
for(var i=52;i<65;i++){
try { //...this line and...
var viewId = dashboard.getRange(i,13).getValue(); // Your Google Analytics view ID
var metric = 'ga:metric, ga:metric2, ga:metric3';
var option = {'segment': 'gaid::-5'};
var result = Analytics.Data.Ga.get(viewId, metric, option);
var metric = result.totalsForAllResults['ga:metric'];
var metric2 = result.totalsForAllResults['ga:metric2'];
var metric3 = result.totalsForAllResults['ga:metric3'];
var doc = SpreadsheetApp.getActiveSpreadsheet(); // Current document
var sheet = doc.getActiveSheet(); // Current sheet
sheet.getRange(i,14,1,1).setValue(metric);
sheet.getRange(i,15,1,1).setValue(metric2);
sheet.getRange(i,16,1,1).setValue(metric3);
} catch(e) { //...this part
console.log(e); //optional, catch(e){} is perfectly valid as well, or any code you might want to execute on error
}
} }

How to get active user email with installabled onEdit trigger? [duplicate]

This question already has an answer here:
onEdit trigger doesn't catch current user
(1 answer)
Closed 3 months ago.
I have a Google spreadsheet with some data. I wrote script to track changes of some specific columns.
function onOpen() {
var ss = SpreadsheetApp.getActive();
var menuItems = [
{name: 'Turn on', functionName: 'createSpreadsheetEditTrigger'}
];
ss.addMenu('Tracker', menuItems);
}
function changeTrack(){
const ss = SpreadsheetApp.getActiveSpreadsheet();
const ui = SpreadsheetApp.getUi();
var ws = ss.getActiveSheet();
const headerRow = 4;
const editBodyCols = [2, 3, 4, 5];
const fResultCol = 6;
var range = ws.getActiveRange();
var row = range.getRow();
var col = range.getColumn();
let target1 = ws.getRange(row, fResultCol);
let target2 = ws.getRange(row, fResultCol + 1)
let activeUser = getCurrentUserEmail();
if(row > headerRow && editBodyCols.some(x => x === col) === true){
if(target1.getValue() !== ""){
target2.setValue(result(ss, ws, row, activeUser)[1]);
} else {
target1.setValue(result(ss, ws, row, activeUser)[0])
target2.setValue(result(ss, ws, row, activeUser)[1])
}
}
}
function createSpreadsheetEditTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger('changeTrack')
.forSpreadsheet(ss).onEdit()
.create();
}
function date(){
return Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss");
}
function result(ss, ws, row, activeUser) {
const ssName = ss.getName();
let data = `Создал ${activeUser} ${date()}`;
let exp = `Файл ${ssName}, Лист ${ws.getName()}, изменил ${activeUser}, строка № ${row}, ${date()}`;
let adds = [];
adds.push([data],[exp]);
return adds;
}
function getCurrentUserEmail()
{
var email=Session.getActiveUser().getEmail();
return email;
}
My problem is to get active user's email. This script can get it but not all the time. Seems like random success. It means sometimes I can get expected value, sometimes not. I don't understand what is it depends from.
Where I'm wrong and how to fix it?
From the documentation on Session.getActiveUser():
Gets information about the current user. If security policies do not allow access to the user's identity, User.getEmail() returns a blank string. The circumstances in which the email address is available vary: for example, the user's email address is not available in any context that allows a script to run without that user's authorization, like a simple onOpen(e) or onEdit(e) trigger, a custom function in Google Sheets, or a web app deployed to "execute as me" (that is, authorized by the developer instead of the user).
So this seems pretty expected and there is no hard workaround you can make to retrieve the users mail. You should maybe just ask for it and see if they be willingly want to give it to you.
Although if you are the developer or the users are inside your organization this restrictions may be ignored:
However, these restrictions generally do not apply if the developer runs the script themselves or belongs to the same G Suite domain as the user.
Based on the comment by b-frid.
The flow:
create a custom menu and tell each user to run the function twice. First time for authorization, and the second time to actually run the code.
this will install the same trigger with the user's privileges and let the onEdit trigger get the email of an active user (author of the trigger)
video-instruction:
https://www.loom.com/share/30b11e4d012447f7a1efdd9a7eac4fca
gist:
https://gist.github.com/Max-Makhrov/e3e89e7fe0c6e86b68a4be1262e53629
Code:
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('😎📬 run me 2x times')
.addItem('please let script to see your email', 'install')
.addToUi();
}
// function test() {
// var e = {
// range: SpreadsheetApp.getActive().getSheetByName('test_onEdit').getRange('B2')
// }
// edit_(e)
// }
function edit_(e) {
var targetcol = 1;
if (e.range.getSheet().getName() === 'Sheet1') {
var user = Session.getActiveUser().getEmail();
if (user !== '') {
var col = e.range.getColumn();
var rows = e.range.getHeight();
var ratgetrange = e.range.offset(
0,
targetcol - col,
rows,
1);
ratgetrange.setValue(user);
}
}
}
function install() {
setOnEditTrigger_(
SpreadsheetApp.getActive().getId(),
'edit_');
}
/**
* create onEdit trigger
*
* #param {string} spreadsheetId
* #param {string} functionName
*
*/
function setOnEditTrigger_(spreadsheetId, functionName) {
console.log('OnEdit trigger ' + functionName +
' for new file' +
spreadsheetId);
var trigger;
if (existsOnEditTrigger_(functionName, spreadsheetId)) {
console.log('stopped execution. Trigger exists.');
return;
}
trigger = ScriptApp
.newTrigger(functionName)
.forSpreadsheet(spreadsheetId)
.onEdit()
.create();
console.log('Created new trigger!')
return trigger.getUniqueId();
}
/**
* check if onEdit trigger exists
*
* #param {string} spreadsheetId
* #param {string} functionName
*
*/
function existsOnEditTrigger_(functionName, spreadsheetId) {
var triggers = ScriptApp.getProjectTriggers();
var trigger = {};
for (var i = 0; i < triggers.length; i++) {
trigger = triggers[i];
if (
trigger.getHandlerFunction() === functionName &&
trigger.getTriggerSourceId() === spreadsheetId &&
trigger.getEventType().toString() === 'ON_EDIT') return true;
}
return false;
}
Original comment:
The key is separate triggers for each user. Then I simply added code
to execute the update only if the Session.getActiveUser().getEmail()
call does not come back blank. Of course, because each's user's
trigger will run, the function will execute x times where x = the
number of users (i.e. triggers), but with the check for a blank return
value the logic only runs once (and so no overwrites). A bit clumsy
indeed, and perhaps not practical if you have more than a handful of
users, but workable in my case.

I am getting an unexpected error when trying to write a Javascript object

I get the following message when I try to save this code from the script editor on Google Sheets: "We're sorry, an unexpected error occurred during compilation." The code creates a class with a method.
If it doesn't create a method I don't get the error, but the point of the object is the method. I did this in the past in another Sheets application, but I can't even get it to work for a simple example.
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet(); // curr active spreadsheet
var theSheet = ss.getActiveSheet(); // current active sheet
var aLogger = tLogger(theSheet, 5, 1, 5); // set up a logger
aLogger.tLog("A test line: 1");
}
// object to do logging on a sheet
function tLogger(ourSheet, startLine, startCol, logLength) {
// object properties:
this.curline = startLine;
this.sheetRef = ourSheet;
this.theColumn = startCol;
this.maxLog = startLine + logLength;
ourSheet.getRange(startLine, startCol, logLength, 1).clear(); // clear old
// including this is what causes the error:
function this.tLog(tText) { // log an entry on the sheet
this.sheetRef.getRange(this.curline++, this.theColumn).setValue(tText);
return;
}
return;
}
Re-code it with ES6 something like above
class tLogger {
constructor(ourSheet, startLine, startCol, logLength) {
this.curline = startLine;
this.sheetRef = ourSheet;
this.theColumn = startCol;
this.maxLog = startLine + logLength;
}
}

Add an Editor for script to run, then Remove them - temporary Editor permission

I shared a spreadsheet with a sheet named "times".
This sheet is range protected to other users, but them must view and have to sort it in several ways.
I create some menus with menuEntries.push etc...
wrote the scripts for sort this sheet in all the ways i need,
but only people I set as administrator can sort using my menu.
The others can't to do it cause they can't execute the script on range protected.
I would like to grant permission to everybody only during the script exectuting,
the code should sound something like this above ( that don't works )
function Editors() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var targetSheet = ss.getSheetByName("times");
var user = SpreadsheetApp.getActive().getEditors();
var permission = targetSheet.getSheetProtection();
permission.addUser(user);
SpreadsheetApp.flush();
var tableRange = "orario!b7:ap209";
var tableRange = "times";
var range = ss.getRange(tableRange);
range.sort( { column: 2, ascending: true } );
permission.removeUser(user)
targetSheet.setSheetProtection(permission)
}
...if someone can help me .... thanks in advance....
I create a simply sheet to test the script.
Here it is:
https://docs.google.com/spreadsheets/d/1DhDo_1A20tWdSaYMknAmuxnVJFW14QY6xBXr9BGyS-g/edit#gid=0
If I use the "menuTeacher" 2. and 3. as admin to sort the sheet
it works correctly.
but as user it doesn't work
( use only the menu "1.sortSURNAME" to test the permissions )
I protect columnA:B and columnE:M
I put Mr Sandy Good as admin like me ( with his mail xxxtrashmatXXX#gmail.com )
to make tests....
You should publish the Project to execute the script as you. Save a version, then publish.
In the script editor, choose FILE, MANAGE VERSIONS. Save a new version. Then choose, PUBLISH, DEPLOY AS WEB APP. Make the settings. That's the only thing I can think of.
The getSheetProtection() method has been deprecated. Don't use it.
Also,
sheet.setSheetProtection(permission);
is also deprecated. Don't use that.
And
var user = ss.getEditors();
Gets an array of users. That's not what you want. You want the current user. For that, you must use the Session class.
var user = Session.getActiveUser().getEmail();
And you need to use addEditor() not addUser()
permission.addEditor(user);
Code
function Editors() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var targetSheet = ss.getSheetByName("Sheet1");
var user = Session.getActiveUser().getEmail();
//Cancel all protections
var protections = targetSheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
for (var i = 0; i < protections.length; i++){
var thisProtection = protections[i];
if (thisProtection && thisProtection.canEdit()) {
thisProtection.remove();
};
};
var permission = targetSheet.protect();
permission.addEditor(user);
SpreadsheetApp.flush();
var tableRange = "orario!b7:ap209";
var tableRange = "times";
var range = ss.getRange(tableRange);
range.sort( { column: 2, ascending: true } );
permission.removeEditor(user);
}
Well, I poste the complete code and make a summary of the problem:
function onOpen() {
var ss = SpreadsheetApp.getActive();
// also tried with var ss = SpreadsheetApp.getActiveSpreadsheet();
menuEntries.push({name: "1. sortSURNAME", functionName: "Cognome"});
ss.addMenu("menuTeacher", menuEntries);
}
function Cognome(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var targetSheet = ss.getSheetByName("orario");
var user = Session.getActiveUser().getEmail();
var protections = targetSheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
var permission = targetSheet.protect();
permission.addEditor(user);
SpreadsheetApp.flush();
for (var i = 0; i < protections.length; i++)
{
var thisProtection = protections[i];
if (thisProtection && thisProtection.canEdit()) {
thisProtection.remove();
}
}
var tableRange = "orario!b7:ap209";
var range = ss.getRange(tableRange);
range.sort( { column: 2, ascending: true } );
permission.removeEditor(user);
}
executing this code a red alert advise that:
" TO PROTECT THIS SHEET, YOU HAVE TO REMOVE PROTECTED RANGE" and the line marked in the script is this one:
var ss = SpreadsheetApp.getActiveSpreadsheet();
So, it seems impossible to remove a protected range with a script if you are an user and not the admin.
Mr Sandy Good suggest to create a web app to execute the script as ADMIN even if you are an USER
I generate it but I don't know how to use it....
Here is the URL of the web app I generate
https://docs.google.com/spreadsheets/d/1DhDo_1A20tWdSaYMknAmuxnVJFW14QY6xBXr9BGyS-g/edit#gid=0

Categories