How to get last execution time of a google app script? - google-apps-script

Does Google App Scripts provide an API to determine when a script was executed the last time?
I'm executing a script periodically and I only like to process data which is new since the last execution, thus I'd like to know when my script was executed the last time. But since I execute it not every x min its hard to hardcode this value.
Thanks in advance, I really appreciate your help and expertise.

I believe your goal as follows.
You want to retrieve the last executed time of the functions of Google Apps Script.
You want to achieve this using Google Apps Script.
In this case, I thought that the method of "Method: processes.list" of Google Apps Script API might be able to be used. In order to use this API, please do the following flow.
Usage:
1. Linking Google Cloud Platform Project to Google Apps Script Project.
In this case, at first, it is required to link Google Cloud Platform Project to Google Apps Script Project. For this, you can see the detail flow at this repository
2. Prepare sample script.
Please copy and paste the following script to the script editor of the Google Apps Script project linking to Google Cloud Platform Project.
function myFunction() {
const scriptId = "###"; // Please set the Google Apps Script project.
const url = "https://script.googleapis.com/v1/processes?pageSize=1&userProcessFilter.scriptId=" + scriptId;
const res = UrlFetchApp.fetch(url, {headers: {authorization: "Bearer " + ScriptApp.getOAuthToken()}});
const obj = JSON.parse(res.getContentText());
const lastExecutedProcess = obj.processes[0];
console.log(lastExecutedProcess)
}
In this sample script, the last executed process of the specific Google Apps Script project can be retrieved. For example, you can also retrieve the last executed process of the specific function of the the specific Google Apps Script project.
When an error occurs, please check https://github.com/tanaikech/Linking-Google-Cloud-Platform-Project-to-Google-Apps-Script-Project-for-New-IDE#sample-1. You can see the method for avoiding the error using Apps Script API.
Note:
For example, if you want to retrieve the last executed process of all functions of all Google Apps Script project in your Google Drive, you can also use the following script.
function myFunction2() {
const url = "https://script.googleapis.com/v1/processes?pageSize=1";
const res = UrlFetchApp.fetch(url, {headers: {authorization: "Bearer " + ScriptApp.getOAuthToken()}});
const obj = JSON.parse(res.getContentText());
const lastExecutedProcess = obj.processes[0];
console.log(lastExecutedProcess)
}
In this API, you can also use from outside of Google. So, you can also achieve above using other languages except for Google Apps Script.
References:
Method: processes.list
Linking Google Cloud Platform Project to Google Apps Script Project for New IDE

I think it is more reliable to use the exact time up to which point you processed your data than the script execution time because there could a small but meaningful difference which may result in either not accounting for some time period or double process some time period.
What you should do instead is to store the exact time up to which you processed the data and store that and then read that value again next time your script runs.
There is an Apps Script service to do that called Properties Service. It is a key/value service to store some basic information in different scopes. What you need is the Script properties so that you can share information between two executions. Something like this:
const propService = PropertiesService.getScriptProperties();
const lastExecutionTime = propService.getProperty('last_execution');
if (lastExecutionTime) {
// Script was executed before.
} else {
// Script has never been executed.
}
const currentTime = Date.now();
// Do your work
propService.setProperty('last_execution', currentTime);

may 2022
if you're not using Google Cloud Platform, the easiest solution might be to add this line of code to all scripts
SpreadsheetApp.openById('id of a log file').getSheets()[0].appendRow([arguments.callee.name,new Date()])
you will find in the log not only the last execution time, but all the use of the script concerned

Related

How to transfer data from webapp to addon

Project_1 is a container-bound script. A container is a readable spreadsheet (Template).
Code_1:
function doPost(e) {
return HtmlService.createHtmlOutput(JSON.stringify(e));
}
The user makes a copy of the Template, deploys the script (Project_1) as a webapp with permissions: "Execute as: Me, Who has access: Anyone". The user is the owner of Project_1.
Project_2 is a script deployed as an add-on. The user from point 1 is not the owner of Project_2.
Code_2:
function sendPost() {
var sheetURL = SpreadsheetApp.getActiveSpreadsheet().getUrl();
var webAppUrl = "https://script.google.com/macros/s/###/exec"; // 7: Part_1 - WebApp: Tester
// var auth = ScriptApp.getOAuthToken();
// var header = { 'Authorization': 'Bearer ' + auth };
var payload = { scriptName: 'updateData', sheetURL: 'sheetURL' };
var options = {
method: 'post',
// headers: header,
muteHttpExceptions: true,
payload: payload
};
var resp = UrlFetchApp.fetch(webAppUrl, options);
var respTxt = resp.getContentText();
console.log('resp: ' + respTxt);
}
function doPost(ev) {
var respWebapp = func(ev);
}
The user installs an add-on (Project_2).
The flow in the direction of addon -> webapp is fine: when sendPost() starts, it sends a request to the webapp and receives a response with the necessary data_1 in response.
The flow in the direction of "someone on the web" -> webapp also flows well: when requesting a webapp_url receives the transferred data_2.
I am trying to transfer data_2 to an addon.
I read a lot about scripts.run, but it seems that this option is not applicable in such a situation.
There is also nowhere to add an eventListener.
I would not want to deploy webapp from my account, so as not to spend my quota for simultaneous executions (<= 30).
Also I would not like to do a sidebar, etc. in the spreadsheet and try to screw eventListener to html. I assume that with this approach, the listener (if it is possible to add it there at all) will be active only when ui is active (the spreadsheet is open and the sidebar is active). Data can come at any time of the day and must be immediately transferred to the addon.
Added:
I feel like I'm stumped. Therefore I reaches out to the community in the hope that someone would suggest a workaround or a new strategy for this initial data. By initial data I mean provide the opportunity for more than 30 users to exchange messages in both directions Spreadsheet <--> External service (for example, Telegram) and at the same time not fall under the limit of 30 simultaneous script executions.
Added_2:
I'm going to assign a bounty, so I'm transferring here from the comments what is missing in the post and updating the question itself.
I rejected the option with immediate entry into the sheet, because this will cause constant calls to the spreadsheet and slow down the performance of the system as a whole.
I am aware of the existence of Google cloud functions and Google compute engine, but would like to stay within the free quotas.
QUESTION: How to transfer data from webapp to addon and execute func () or which workaround to use to achieve the goals?
Here is a list of your requirements:
Trigger add-on code to run from some external request, not using the add-on user interface or time based trigger.
Code runs from the user's account, using their quota
Run the add-on code regardless of whether the user is using the add-on or not. For example, their Google Sheet is closed, and the user may even be signed out.
I only know of one way to do that, and it's with a Sheet's add-on by triggering the "On Change" event by setting a value in a Sheet cell using the Sheets API. The Sheets API must use a special option to set the value "As the User."
The special setting is:
valueInputOption=USER_ENTERED
That option will trigger the "On Change" event even if the Sheet is closed.
Obviously the script making the request needs authorization from the user to set a value in a cell of the Sheet.
If the script sending the request is outside of the user's account then you'd need to use OAuth.
The add-on would need to install an "On Change" trigger for the Sheet and the function that the trigger is bound to would need to determine whether the change was from the special cell designated for this special functionality.
If the request to set a value in the users Sheet is from outside of that users Google account, then the user of the Sheet would need to somehow authorize the OAuth credentials for the Sheets API to make a change to the Sheet.
Depending upon the programming language being used with the Google Sheets API, there may be a Sheets API Library specifically for that language. You can also use the Sheets REST API.
There is an example here on StackOverflow of using the Sheets REST API from Apps Script, but if the external request is from some code that isn't Apps Script, it won't be exactly the same.
I understand that the solutions proposed in the comments, by others and myself, can't work in your scenario because it can't stand an average delay of 30 seconds. In that case I strongly advise you to set up a Cloud project that can be used as an instant server, as opposed to triggers/apps/etc.
In "Code_1" and "Code_2" use a shared data store. In other words, instead of directly passing the data from "Code_1" to "Code_2", make that Code_1 write to the datastore and "Code_2" read from it.
One possibility among many is to use a spreadsheet as a database. In this case you might use on change triggers to do some action when the spreadsheet is changed by one of the "Code_1" scripts and/or use time-driven triggers to do some action with certain frequency or at certain datetime.

Using private sheets with tabletop.js

I've used tabletop.js [1] in the past and is amazing! You can simply do anything you want seriously.
The only problem I saw is that you need to publish your spreadsheets to the web, which of course is really risky if you are working with sensitive data.
I'm in need now of using it in a project with sensitive data, so I was hoping someone can guide me on how to use it with spreadsheets that are not published to the web.
I've been searching for this for a long time without any success but seems that tabletop.js does support private sheets (here's the pull request that added this option [2]).
In fact, looking at the documentation they included it [1]:
authkey
authkey is the authorization key for private sheet support.
ASK: How am I suppose to use the authkey? can someone provide me with an example so I can try?
Thanks in advance!
[1] https://github.com/jsoma/tabletop
[2] https://github.com/jsoma/tabletop/pull/64
How about this answer?
Issue and workaround:
At "tabletop.js", from the endpoint (https://spreadsheets.google.com/feeds/list/###/###/private/values?alt=json) of request, it seems that "tabletop.js" uses Sheets API v3. And when authkey is used, oauth_token=authkey is added to the query parameter. In this case, unfortunately, it seems that the private Spreadsheet cannot be accessed with it. From this situation, unfortunately, I thought that in the current stage, "tabletop.js" might not be able to use the private Spreadsheet. But I'm not sure whether this might be resolved in the future update. Of course, it seems that the web-published Spreadsheet can be accessed using this library.
So, in this answer, I would like to propose the workaround for retrieving the values from Spreadsheet as the JSON object.
Pattern 1:
In this pattern, Google Apps Script is used. With Google Apps Script, the private Spreadsheet can be easily accessed.
Sample script:
When you use this script, please copy and paste it to the script editor and run the function myFunction.
function myFunction() {
const spreadsheetId = "###"; // Please set the Spreadsheet ID.
const sheetName = "Sheet1"; // Please set the sheet name.
const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);
const values = sheet.getDataRange().getValues();
const header = values.shift();
const object = values.map(r => r.reduce((o, c, j) => Object.assign(o, {[header[j]]: c}), {}));
console.log(object) // Here, you can see the JSON object from Spreadsheet.
}
I thought that this might be the simple way.
Pattern 2:
In this pattern, the Web Apps created by Google Apps Script is used. When the Web Apps is used, the private Spreadsheet can be easily accessed. Because the Web Apps is created with Google Apps Script. In this case, you can access to the Web Apps from outside by logging in to Google account. And, the JSON object can be retrieved in HTML and Javascript.
Usage:
Please do the following flow.
1. Create new project of Google Apps Script.
Sample script of Web Apps is a Google Apps Script. So please create a project of Google Apps Script. In order to use Document service, in this case, Web Apps is used as the wrapper.
If you want to directly create it, please access to https://script.new/. In this case, if you are not logged in Google, the log in screen is opened. So please log in to Google. By this, the script editor of Google Apps Script is opened.
2. Prepare script.
Please copy and paste the following script (Google Apps Script) to the script editor. This script is for the Web Apps.
Google Apps Script side: Code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile("index");
}
function getObjectFromSpreadsheet(spreadsheetId, sheetName) {
const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);
const values = sheet.getDataRange().getValues();
const header = values.shift();
const object = values.map(r => r.reduce((o, c, j) => Object.assign(o, {[header[j]]: c}), {}));
return object;
}
HTML&Javascript side: index.html
<script>
const spreadsheetId = "###"; // Please set the Spreadsheet ID.
const sheetName = "Sheet1"; // Please set the sheet name.
google.script.run.withSuccessHandler(sample).getObjectFromSpreadsheet(spreadsheetId, sheetName);
function sample(object) {
console.log(object);
}
</script>
spreadsheetId and sheetName are given from Javascript side to Google Apps Script side. From this situation, in this case, getObjectFromSpreadsheet might be instead of "tabletop.js".
3. Deploy Web Apps.
On the script editor, Open a dialog box by "Publish" -> "Deploy as web app".
Select "Me" for "Execute the app as:".
By this, the script is run as the owner.
Select "Only myself" for "Who has access to the app:".
In this case, in order to access to the Web Apps, it is required to login to Google account. From your situation, I thought that this might be useful.
Click "Deploy" button as new "Project version".
Automatically open a dialog box of "Authorization required".
Click "Review Permissions".
Select own account.
Click "Advanced" at "This app isn't verified".
Click "Go to ### project name ###(unsafe)"
Click "Allow" button.
Click "OK".
Copy the URL of Web Apps. It's like https://script.google.com/macros/s/###/exec.
When you modified the Google Apps Script, please redeploy as new version. By this, the modified script is reflected to Web Apps. Please be careful this.
4. Run the function using Web Apps.
You can test above scripts as follows.
Login to Google account.
Access to the URL of Web Apps like https://script.google.com/macros/s/###/exec using your browser.
By this, you can see the retrieved JSON object at the console.
Note:
When you modified the script of Web Apps, please redeploy the Web Apps as new version. By this, the latest script is reflected to the Web Apps. Please be careful this.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script

Is it possible to get fresh Google Slides presentation data in AppsScript?

It seems that if one uses SlidesApp.getActivePresentation() in AppsScript, the result of the function is not fresh but rather something that was already prepared beforehand.
Scenario
Imagine you have two users performing the following function in AppsScript simultaneously:
function updateSlideText(slideId) {
// Request exclusive write access to the document
var lock = LockService.getDocumentLock();
lock.waitLock(15000);
// Perform changes
var presentation = SlidesApp.getActivePresentation();
var textBox = presentation.getSlideById(MY_SLIDE_ID).getPageElementById(MY_TEXTBOX_ID);
textBox.asShape().getText().setText('My text');
// Save and release lock
presentation.saveAndClose();
lock.releaseLock();
}
If this function is called twice at the same time, the resulting slide contains text "My textMy text".
When I add Utilities.sleep(10000) just before the lock release, it delays the 2nd execution by 10s but after those 10s I still end up with the same result. On the other hand, if I actually delay calling the function 10s, the output is fine.
From this I conclude that it does not matter if I call saveAndClose and use locks. Once the function is called, it will always have stale data. Is there a way around this? Is it not possible to request that fresh data will be loaded after the lock is aquired?
More details
Some more pseudo-code to better illustrate the problem use-case:
// The addon frontend
websocket.onMessage((message) => {
if (message.type === 'pollUpdate') {
const slideWithPoll = store.getState().slides.find(
slide => slide.pollId === message.pollId
);
if (slideWithPoll.title !== message.poll.title) {
google.script.run.updateSlideText(slideWithPoll.id, message.poll.title);
}
}
});
I believe your goal as follows.
When 2 users are run your script for Google Slides, simultaneously, you want to run the script individually.
For this, how about this answer?
Issue and workaround:
When I tested your situation, I could confirm the same issue like My textMy text. When I tested several times, in this case, I thought that the LockService might not affect to Google Slides. So as a workaround, I would like to propose to use Web Apps as the wrapper. Because it has already been known that Web Apps can run exclusively by the LockService. The flow of this workaround is as follows.
When the script is run, the script requests to Web Apps.
At Web Apps, your script is run.
By this, even when the script is run, simultaneously, the script can be exclusively run with the LockService.
Usage:
The usage of this sample script is as follows. Please do the following flow.
1. Prepare script.
When your script is used, it becomes as follows. Please copy and paste the following script to the script editor. Please set MY_SLIDE_ID and MY_TEXTBOX_ID.
function doGet() {
// This is your script.
var presentation = SlidesApp.getActivePresentation();
var textBox = presentation.getSlideById(MY_SLIDE_ID).getPageElementById(MY_TEXTBOX_ID);
var text = textBox.asShape().getText();
text.setText('My text');
return ContentService.createTextOutput("ok");
}
// Please run this function.
function main() {
var lock = LockService.getDocumentLock();
if (lock.tryLock(10000)) {
try {
const url = "https://script.google.com/macros/s/###/exec"; // Please set the URL of Web Apps after you set the Web Apps.
const res = UrlFetchApp.fetch(url);
console.log(res.getContentText())
} catch(e) {
throw new Error(e);
} finally {
lock.releaseLock();
}
}
}
2. Deploy Web Apps.
On the script editor, Open a dialog box by "Publish" -> "Deploy as web app".
Select "Me" for "Execute the app as:".
By this, the script is run as the owner.
Select "Anyone, even anonymous" for "Who has access to the app:".
In this case, no access token is required to be request. I think that I recommend this setting for testing this workaround.
Of course, you can also use the access token. At that time, please set this to "Anyone". And please include the scope of https://www.googleapis.com/auth/drive.readonly and https://www.googleapis.com/auth/drive to the access token. These scopes are required to access to Web Apps.
Click "Deploy" button as new "Project version".
Automatically open a dialog box of "Authorization required".
Click "Review Permissions".
Select own account.
Click "Advanced" at "This app isn't verified".
Click "Go to ### project name ###(unsafe)"
Click "Allow" button.
Click "OK".
Copy the URL of Web Apps. It's like https://script.google.com/macros/s/###/exec.
When you modified the Google Apps Script, please redeploy as new version. By this, the modified script is reflected to Web Apps. Please be careful this.
Please set the URL of https://script.google.com/macros/s/###/exec to url of above script. And please redeploy Web Apps. By this, the latest script is reflected to the Web Apps. So please be careful this.
4. Test this workaround.
Please run the function of main() by 2 users, simultaneously as you have tested. By this, it is found that the script is run exclusively. In my environment, in this case, I confirmed that even when the LockService is not used, the script is exclusively run. But I would like to recommend to use the LockService just in case.
Note:
This is a simple sample script for explaining this workaround. So when you use this, please modify it for your actual situation.
About the situation that the LockService might not affect to Google Slides, in the current stage, although I'm not sure whether this is the bug, how about reporting this to the Google issue tracker? Unfortunately, I couldn't find this issue at the current Google issue tracker.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script

Google Sheets: How to import the following data?

Data Source
https://www.treasury.gov/resource-center/data-chart-center/interest-rates/Pages/TextView.aspx?data=yieldAll
I am trying to get the following data onto a Google Sheet, but it is looking to be tricky to do so using IMPORTXML. Any idea how to do it?
You want to retrieve a table from the HTML data of the URL.
From I am trying to get the following data onto a Google Sheet, I thought like this.
If my understanding is correct, how about this answer?
Issue and workaround:
Unfortunately, it seems that the file size of HTML is large. So when =IMPORTXML("https://www.treasury.gov/resource-center/data-chart-center/interest-rates/Pages/TextView.aspx?data=yieldAll","//title") is used, an error of Resource at url contents exceeded maximum size. occurs. When I retrieve HTML data from the URL, the size of HTML data was about 9 MB. It is considered that the reason of error is due to this. So as one of workaround, how about using Google Apps Script? In this workaround, the following flow is used.
Retrieve HTML data using UrlFetchApp
Parse the HTML data using Parser which is a GAS library.
Put the parsed data to the active sheet on the Spreadsheet using PasteDataRequest of Sheets API.
Usage:
Preparation:
Please install Parser. About the install of library, you can see it at here.
The project key of the library is M1lugvAXKKtUxn_vdAG9JZleS6DrsjUUV.
Please enable Sheets API at Advanced Google services.
Sample script:
Please copy and paste the following script to the script editor of the container-bound script of the Spreadsheet. After above settings were done, please run the function of myFunction(). When the script is run, the table of HTML is put to the active sheet on the Spreadsheet.
function myFunction() {
// Retrieve HTML data from URL.
var url = "https://www.treasury.gov/resource-center/data-chart-center/interest-rates/Pages/TextView.aspx?data=yieldAll";
var html = UrlFetchApp.fetch(url).getContentText();
// Parse HTML data.
var table = "<table" + Parser.data(html).from("<table class=\"t-chart\"").to("</table>").build() + "</table>";
// Put the values to the Spreadsheet.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var resource = {requests: [{pasteData: {html: true, data: table, coordinate: {sheetId: sheet.getSheetId()}}}]};
Sheets.Spreadsheets.batchUpdate(resource, ss.getId());
}
References:
Parser
PasteDataRequest
Advanced Google services
If I misunderstood your question and this was not the direction you want, I apologize.
Updated at April, 23, 2021:
New IDE for Google Apps Script has finally been released at December 7, 2020. Ref By this, in the current stage, in order to install Google Apps Script library, it is required to use the script ID of Google Apps Script project.
In this case, when the Google Apps Script library of Parser is installed, unfortunately, this ID M1lugvAXKKtUxn_vdAG9JZleS6DrsjUUV cannot be used.
So when you use new IDE, please use the following script ID.
1Mc8BthYthXx6CoIz90-JiSzSafVnT6U3t0z_W3hLTAX5ek4w0G_EIrNw
This script ID is the ID of Google Apps Script project of M1lugvAXKKtUxn_vdAG9JZleS6DrsjUUV. By this, the library of Parser can be installed to the new IDE.
About the method for installing the library, you can see the official document.
Reference:
Libraries

Google Sheets, "The request is missing a valid API key"

I am trying to use the Google Sheets API. The problem is, once I call my script function on the google spreadsheet, I get the following error:
API call to sheets.spreadsheets.values.get failed with error: The request is missing a valid API key. (line 5).
where line 5 in the script looks like this:
var values = Sheets.Spreadsheets.Values.get(spreadsheetId, rangeName).values;
and spreadsheetId and rangeName are defined in the first lines.
I think the problem might be that I did not copy the API key anywhere, but I really do not understand where and how I can do it.
I call the function just using = function().
When you use Sheets API by a custom function like =myFunction() put to a cell, such error occurs. When the custom function is run, ScriptApp.getOAuthToken() returns null. I think that this is the mainly reason of your issue. And unfortunately, I think that this is the specification. In order to avoid this issue, I would like to propose 2 workarounds.
Workaround 1:
A part of Spreadsheet Services can be used at the custom function. So using this, it obtains the same result with var values = Sheets.Spreadsheets.Values.get(spreadsheetId, rangeName).values;. In the case of your script, openById() cannot be used. So the script is as follows.
Sample script:
function customFunc() {
var rangeName = "#####"; // Please set this.
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var values = sheet.getRange(rangeName).getValues();
return values;
}
Workaround 2:
If you want to use Sheets API, the access token is required. But it is required to directly request to the endpoint of Sheets API, because the access token is automatically used in internal at Advanced Google Services. As an issue of this case, there is that when ScriptApp.getOAuthToken() is run in the custom function, null is returned. In order to avoid this, as a method, the access token is directly given to the custom function using PropertiesService. The sample flow is as follows.
When the Spreadsheet is opened, it puts the access token to PropertiesService by the OnOpen trigger.
When you use this, please install the OnOpen trigger to onOpenFunc() in the sample script.
When the custom function is run, the access token retrieved by PropertiesService is used for using Sheets API.
By this, Sheets API can be used in the custom function.
Sample script:
// Please install OnOpen trigger to this function.
function onOpenFunc() {
PropertiesService.getScriptProperties().setProperty("accessToken", ScriptApp.getOAuthToken());
}
function customFunc() {
var spreadsheetId = "#####"; // Please set this.
var rangeName = "#####"; // Please set this.
var accessToken = PropertiesService.getScriptProperties().getProperty("accessToken");
var url = "https://sheets.googleapis.com/v4/spreadsheets/" + spreadsheetId + "/values/" + rangeName;
var res = UrlFetchApp.fetch(url, {headers: {"Authorization": "Bearer " + accessToken}});
var obj = JSON.parse(res.getContentText());
var values = obj.values;
return values;
}
The expilation time of access token is 1 hour. In this sample script, PropertiesService is used. In this case, when 1 hour is spent after the Spreadsheet was opened, the access token cannot be used. If you want to use continuously the access token, you can also update it using the time-driven trigger.
Note:
When you use Sheets API, please enable Sheets API at API console.
References:
Custom Functions in Google Sheets
spreadsheets.values.get
PropertiesService
If these workarounds were not what you want, I apologize.
I want to thank you, #Tanaike, for your response (I don't have enough 'points' to upvote or comment, so my only option is an 'Answer')
I know this thread is several years old, but I thought others might be interested in my personal experience.
First of all: "Workaround 1" worked for me!
The function/method "Sheets.Spreadsheets.Values.get(spreadsheetID, RangeName).values" was giving me an "missing a valid API key" error, so I swapped it for "sheet.getRange(RangeName).getValues()".
Most of the above was set as Global Variables, i.e. outside of any functions.
Weird thing was that this error occured only when running from within the [container] sheet, not from the "embedded" script.
For instance: If I had an active onOpen() function, and I opened/refreshed the Sheet, the script would log a "The request is missing a valid API key." error, and the UI/Menu I had built therein would not load.
I could, however run the onOpen() function from within the script itself, and the menu would appear, and function, within the Sheet. If I disabled/renamed the onOpen() function, and reloaded the Sheet, I would not get the error message.
Makes sense, as the simple loading of the Sheet does not appear to run the script, but when one does access it (the script), i.e. through the onOpen() function, then the initial global variables are read (and the error occurs).
However, when I ran the same function, or others, from within the script itself, they would run ok. This permissions conundrum is what has led me on a wild goose chase all over the Internet, ultimately landing here.
All this after numerous other issues, in the course of whose resolution I built a Google Cloud Project, added APIs (e.g. for Sheets), added scopes to the oauthScopes section of the manifest, and more.
It was only after I made the replacements described above that everything worked, both from the script, and its container spreadsheet! So, THANKS!
Anyway... Sorry for the long post, but I hope others may benefit from your solution, and in which context it helped me.
Cheers,
-Paul