0

So I'm relatively new to app-script and have succeeded in making an API request using urlFetchApp and pulling data onto my google sheet. Now what I am interested in doing is having this API request code run whenever I edit the contents of a specific cell. And the contents of the cell will be integrated into the request query. To be more specific, I am pulling company financial reports via an API and I want to be able to enter a new company ticker in a cell and have that company's financials immediately pulled to my sheet by way of the API request.

I understand (from experience) that the simple trigger onEdit(e) does not work due to permission issues, and that I need to create an installable trigger. But for some reason, even though the UrlFetchApp works perfectly when run from the script editor, it fails to pull the same data when triggered by the installable trigger I created. Any advice as to how to get this to work and what I am doing wrong will be very much appreciated.

This is my code:

// make an API request and pull the historical income statements 
// for the company ticker in cell B1 of my sheet
function getIncomeStatement() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var url = 'https://www.alphavantage.co/query?function=INCOME_STATEMENT&symbol='
    + SpreadsheetApp.getActiveSheet().getRange('B1').getValue()
    + '&apikey=*****************';
  var response = UrlFetchApp.fetch(url);
  var financials = JSON.parse(response.getContentText());
  return financials;
}

// get the company's ticker and historic annual income statement reports 
// and print them to the active sheet
function getKeysVals() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var financials = getIncomeStatement();

  // get ticker and paste to sheet
  var symbol = financials['symbol'];
  rgMyRange = sheet.getRange(2, 1, 1, 1);
  rgMyRange.setValue('Requested Ticker');
  rgMyRange = sheet.getRange(2, 2, 1, 1);
  rgMyRange.setValue(symbol);

  // get Income Statement annual reports
  var annualReport = financials['annualReports'];

  // loop over nested objects in annualReports
  colToPaste = 1;
  for (var i = 1; i < annualReport.length; i++) {
    yearKeys = [];
    yearValues = [];

    // loop over keys/values within a specific year and paste keys/values to arrays
    var currentYear = annualReport[i];
    for (var key in currentYear) {
      yearKeys.push(key);
      yearValues.push(currentYear[key]);
    }

    // Combine the 2 arrays into one 2-Dimensional array and paste to sheet
    var values = yearKeys.map(function (e, i) { return [e, yearValues[i]] });
    rgMyRange = sheet.getRange(3, colToPaste, values.length, values[0].length);
    rgMyRange.setValues(values);

    // Move the range by 2 columns to the right avoid overwriting the next year's figures 
    colToPaste = colToPaste + 3;
  }
}

// create an installable trigger for onEdit
function createSpreadsheetEditTrigger() {
  var ss = SpreadsheetApp.getActive();
  ScriptApp.newTrigger('newOnEdit')
      .forSpreadsheet(ss)
      .onEdit()
      .create();
}

// create a new onEdit function to look for changes in cell B1
function newOnEdit(e) {
  var cellAddress = e.range.getA1Notation();
  if (cellAddress === 'B1') {
    getKeysVals();
  }
}
Amos Turin
  • 43
  • 4
  • what is the error you are getting in the executions page? – Marios Feb 24 '21 at 21:33
  • 1
    to see the errors of an onEdit trigger you need to go to the execution page. in the new editor you can find the executions page by clicking the button between the alarm icon and settings icon to the left of the script editor. – Marios Feb 24 '21 at 21:58
  • You cannot run onedit functions from the script editor unless you provide the event object – Cooper Feb 24 '21 at 22:09
  • Ok found it: TypeError: Cannot read property 'length' of undefined at getKeysVals(AlphaVantage:49:36) at newOnEdit(AlphaVantage:104:5) – Amos Turin Feb 24 '21 at 22:09
  • So if I run function getKeysVals() from the script editor it works perfectly and I pull an object from the url into my variable 'financials'. If I update the cell and the trigger fires, an object isn't pulled, instead, if I Logger.log(financials) I get: {Information=Thank you for using Alpha Vantage! Our standard API call frequency is 5 calls per minute and 500 calls per day.} Even though I have not exceeded the daily or minute limit. – Amos Turin Feb 24 '21 at 22:39
  • Can you try this? Straight after this line `var response = UrlFetchApp.fetch(url);` can you insert `Logger.log(response.getResponseCode());` and `Logger.log(response.getContentText())` ? Just to check. From what you have said so far it does seem like Apps Script is not the problem, but if you can get back to us with the result of that then maybe there will be another clue there. – iansedano Feb 25 '21 at 10:50
  • Also, just to be sure, make sure you aren't duplicating your trigger. Check the triggers active on your account to make sure that the onEdit is only running once every time it fires. – iansedano Feb 25 '21 at 10:54
  • I added the Logger.log lines (when run from script editor the response code is 200.0, and the content is the desired object) Also, in my executions after editing the cell, I see the trigger ran twice (3 seconds apart). The first has status completed, the second has status failed. What should I do? – Amos Turin Feb 25 '21 at 11:34

1 Answers1

1

The API is rate limited with burst protection

For many APIs, when they say, for example:

up to 5 API requests per minute

What this actually means is that you can only make one request every 12 seconds. You can't make 5 requests in 10 seconds and then wait for 50 seconds to make another 5 requests.

I am not sure why some APIs are not explicit about this in their documentation, because it is easy to assume that if they say 5 requests per minute, that you can make 5 requests in 0.5 seconds if you like.

A possible reason for this is that it serves as protection against DDOS attacks. Imagine if someone obtained thousands free API keys, and then set up to coordinate each API key to simultaneously send 5 requests. This could break the server.

Workaround

Aside from getting premium membership, you could set a time driven trigger to send a request every 12 seconds to check if there are any stock prices that need checking, and that way, fill them out bit by bit. If it finds no stocks, then it doesn't make a request.

Or you could try an onEdit trigger that stores times with the PropertiesService

function onEdit(e) {
  // Get current time of edit
  let time = new Date();
  // Get the time of last request
  let scriptProperties = PropertiesService.getScriptProperties()
  let property = scriptProperties.getProperty("lastRequest")
  let lastRequest = new Date(property)

  // Calculate time since last request
  let timeSinceLastRequest = time.getTime() - lastRequest.getTime()

  // If more than 12 seconds ago, make request and reassign property
  if (timeSinceLastRequest > 12000) {

    // MAKE THE REQUEST HERE

    scriptProperties.setProperty("lastRequest", new Date())
  }
}


// Run this first to set the property for the first time.
function init(){
  let scriptProperties = PropertiesService.getScriptProperties()
  scriptProperties.setProperty("lastRequest", new Date())
}

References

iansedano
  • 6,169
  • 2
  • 12
  • 24
  • Thank you for this info, really helpful! So at the moment for some reason my trigger is firing twice within 3 seconds. Is there some way to limit it. Say, to prevent it from firing more than once within a given period of time? For example with an IF statement? – Amos Turin Feb 26 '21 at 10:14
  • 1
    @AmosTurin I will update my answer with a short example – iansedano Feb 26 '21 at 11:40
  • So it didn't work unfortunately. Thanks for trying to help though. Surprisingly, when I manually send multiple requests in under 12 seconds they all pull the data with no problem, so I'm not sure why it doesn't work for the installable onEdit trigger. I'm completely at a loss. So, have instead of a trigger just created a new menu on the GUI with a 'make API request' button on it to manually call the API from the sheet itself.. – Amos Turin Mar 02 '21 at 09:06
  • I'm confused now, does it _never_ work with onEdit? – iansedano Mar 02 '21 at 11:43
  • Nope. No matter what I have tried, it never works with onEdit. And I have no idea why. – Amos Turin Mar 03 '21 at 21:22