{"id":10439,"date":"2018-08-16T12:13:42","date_gmt":"2018-08-16T09:13:42","guid":{"rendered":"https:\/\/railsware.com\/blog\/?p=10439"},"modified":"2022-05-10T00:16:20","modified_gmt":"2022-05-09T21:16:20","slug":"google-apps-script-gotchas-to-develop-an-add%e2%80%91on","status":"publish","type":"post","link":"https:\/\/railsware.com\/blog\/google-apps-script-gotchas-to-develop-an-add%e2%80%91on\/","title":{"rendered":"Google Apps Script Gotchas You Should Know to Develop an Add\u2011on"},"content":{"rendered":"<p class=\"intro-text\">If you are reading this blog post, you are probably working on a Google add-on in <a href=\"https:\/\/developers.google.com\/apps-script\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Google Apps Script<\/a>. In this article, I want to share the specific domain knowledge our Railsware development team has gained, as well as the best practices we applied when building the <a title=\"Coupler.io\" href=\"https:\/\/www.coupler.io\/\" target=\"_blank\" rel=\"noopener\">Coupler.io<\/a> Google Sheets add-on.<\/p>\nAt early stages Coupler.io was a Google Sheets add-on used to automate the data import process from Airtable to Google spreadsheets. At some point in time users started complaining about API instability, low speed of work and unstable background sync. It is worth mentioning that back then the product was available on the G Suite Marketplace for free. After analyzing the support tickets, we saw clearly that Airtable users were ready to pay for a more stable version, which pushed us to basically rethink the strategy for this product.\n\nTherefore, below I will focus on the problems we faced and solutions we discovered while improving the add-on stability and integrating the <a href=\"https:\/\/www.chargebee.com\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">ChargeBee<\/a> payment system. In any case, this article will also be useful for those who want to learn Google Apps Script.\n\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-10495\" src=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/GASGotchas-illustration.jpg\" alt=\"Using Google Apps Script to Develop Add-ons\" width=\"2400\" height=\"1260\" srcset=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/GASGotchas-illustration.jpg 2400w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/GASGotchas-illustration-360x189.jpg 360w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/GASGotchas-illustration-768x403.jpg 768w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/GASGotchas-illustration-1024x538.jpg 1024w\" sizes=\"auto, (max-width: 2400px) 100vw, 2400px\" \/>\n<h3>1. How would you find out from the Google Apps Script spreadsheet sidebar what an active sheet is?<\/h3>\nThere is no event in the Google Apps Script API that would allow you to find out from the sidebar what an active sheet is.\n\nSo, if you want to display the relevant information in the spreadsheet sidebar based on a current active sheet &#8211; you have to write a callback function explicitly to do that job.\n\nFirst of all, write the following function in the back end:\n<pre class=\"lang:javascript\">function getCurrentSheetId(){\n  return SpreadsheetApp.getActiveSheet().getSheetId();\n}\n<\/pre>\nThen call that function explicitly from the Google Apps Script UI side using a helper function:\n<pre class=\"lang:javascript\">var currentSheetId = null;\nfunction onCurrentSheetChange(callback){\n    google.script.run\n      .withSuccessHandler(function(sheetId){\n        if(currentSheetId !== sheetId){\n          currentSheetId = sheetId;\n          callback(sheetId);\n        }\n        setTimeout(onCurrentSheetChange.bind(null, callback), 100); \/\/ time in milliseconds to wait\n      })\n      .withFailureHandler(function() {\n        setTimeout(onCurrentSheetChange.bind(null, callback), 100); \/\/ Still continue calling even if error happens\n      }).getCurrentSheetId();\n  }\n<\/pre>\nThe only way to know that the active sheet has been changed is to monitor the add-on\u2019s back end for some period of time (for example, 100ms). Of course, you will experience some delay between the time when a user changes the sheet and the time when a callback runs. In our case, the delay was about 0.5s. After you understand the code written above, all you have to do is to add this small piece of code:\n<pre class=\"lang:javascript\">onCurrentSheetChange(function(sheetId){\n  \/\/ Do whatever you want\n});\n<\/pre>\n<strong>Note:<\/strong> in the majority of cases you will get the following message in the browser console:\n<pre>Net state changed from IDLE to BUSY\nNet state changed from BUSY to IDLE\n<\/pre>\nIt is just a notification about the state of the connection to the add-on&#8217;s back end. So, if you use the above-mentioned method to be notified once a new sheet opens &#8211; you will keep seeing these messages. You can check a <a href=\"https:\/\/stackoverflow.com\/questions\/51142087\/unwanted-output-in-console\/51283865#51283865\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">question<\/a> related to this topic on StackOverflow as well.\n<h3>2. Coding Google Apps Script in your lovely IDE locally<\/h3>\nThere is a way to develop your Google Apps Script locally instead of using the online Google Apps Script editor. You can use the tool called <a href=\"https:\/\/github.com\/google\/clasp\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">clasp<\/a> for this purpose, which will save your time by helping you structure the code.\n\nYou can develop your add-on locally in any IDE you want and by one simple command synchronize it with Google Apps Script.\n<ul>\n \t<li><strong>Develop Locally:<\/strong> clasp enables the development of Apps Script projects locally. Developers can check in their code into source control, cooperate with other devs and use favorite tools.<\/li>\n \t<li><strong>Manage Deployment Versions:<\/strong> Create, update, and view multiple deployments of your project.<\/li>\n \t<li><strong>Structure Code:<\/strong> clasp automatically converts your flat project on <a href=\"https:\/\/script.google.com\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">script.google.com<\/a> into folders. For example:\n<ul>\n \t<li>On script.google.com:\n<ul>\n \t<li>tests\/slides.gs<\/li>\n \t<li>tests\/sheets.gs<\/li>\n<\/ul>\n<\/li>\n \t<li>locally:\n<ul>\n \t<li>tests\/<\/li>\n \t<li style=\"list-style-type: none;\">\n<ul>\n \t<li>slides.js<\/li>\n \t<li>sheets.js<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n \t<li>Write Apps Script in <a href=\"https:\/\/www.typescriptlang.org\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">TypeScript<\/a><\/li>\n<\/ul>\n<h3>3. Google Apps Script HTML service structure. External assets.<\/h3>\nSurely, you would like to use some external JS\/CSS libraries in the add-on\u2019s sidebar, such as different analytics and material design libraries, or trite jQuery.\n\nI wouldn\u2019t suggest you to use many JS\/CSS libraries because they may increase sidebar loading time. If you prefer using jQuery, I would recommend changing it with <a href=\"https:\/\/vladocar.github.io\/nanoJS\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">nanoJS<\/a>. It will work great in most cases if you are building an add-on with simple behavior. Moreover, it\u2019s 7 times smaller in size than jQuery.\n\nThe same approach applies to the material design library. Even though Google propagates its usage, I would recommend trying a cool alternative called <a href=\"https:\/\/materializecss.com\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Materialize<\/a>, which has the same concept, is simple to integrate and has a much smaller size.\n\nRemember that if you open <a href=\"https:\/\/developers.google.com\/apps-script\/reference\/base\/browser#msgBox(String,String,ButtonSet)\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">MsgBox<\/a> (a native modal window of the add-on API) &#8211; it will have no style by default, but you will be able to include everything you need in the Google Apps Script dialog box, as well as in the sidebar UI. As a result, I would suggest splitting your project into different files: at least keep the Google Apps Script HTML, JS, and CSS separately. This is how you can add assets to MsgBox and sidebar UI without duplication.\n\nBelow you may see how our manifest file looks:\n<pre class=\"lang:html\">&lt;!--?!= include('sidebar\/assets\/css'); ?--&gt; \n&lt;!-- Google Apps Script HTML service example that defines the sidebar element structure. --&gt; \n&lt;div class=\"sidebar\"&gt;&lt;!--?!= include('sidebar\/partials\/current_sheet'); ?--&gt; \n&lt;!--?!= includeIf('sidebar\/partials\/dev', ADDON_ENV() !== 'production'); ?--&gt;\n&lt;\/div&gt;\n\n<\/pre>\n<pre class=\"lang:html\">&lt;!--?!= include('sidebar\/assets\/external.js'); ?--&gt; \n&lt;!--?!= include('sidebar\/assets\/helpers.js'); ?--&gt; \n&lt;!--?!= include('sidebar\/assets\/js'); ?--&gt;\n<\/pre>\nAnd here are the helper functions located in the add-on\u2019s back end:\n<pre class=\"lang:javascript\">function include(filename) {\n  return HtmlService.createTemplateFromFile(filename).evaluate().getContent();\n}\n\nfunction includeIf(filename, condition){\n  if(condition){\n    return include(filename);\n  } else {\n    return '';\n  }\n}\n<\/pre>\nCalling <a href=\"https:\/\/developers.google.com\/apps-script\/reference\/html\/html-template\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">.evaluate()<\/a> in the <i>.include()<\/i> function allows you to use <i><!--?!= ?--><\/i> syntax in files that you included in manifest.\n\nPlease bear in mind that you cannot use plain .css or .js files. What you can use are files with a .html extension that you have to put code inside the specific HTML tag.\n\nFor instance, here is how sidebar\/assets\/js.html will look:\n<pre class=\"lang:html\">&lt;script type=\"text\/javascript\"&gt; \/\/ Your js magic goes here &lt;\/script&gt;\n<\/pre>\nAnd, finally, below is the function of evaluating the index.html (manifest file) into sidebar:\n<pre class=\"lang:javascript\">function onOpen(_event) {\n    var sidebar = HtmlService.createTemplateFromFile(\"sidebar\/index\").evaluate().setTitle(\"Google Apps Script sidebar\");\n    SpreadsheetApp.getUi().showSidebar(sidebar);\n}\n<\/pre>\n<h3>4. Staging environment and environment variables<\/h3>\nSome Google Apps Script features, like time-based triggers, are hard to test in the development environment (\u201cTest as add-on\u2026\u201d mode), when you just open the script in one of your files.\n\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-10475\" src=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/test-as-addon.png\" alt=\"\" width=\"222\" height=\"134\">\n\n<em style=\"display: block; text-align: center; font-size: 80%;\">Pic. 1. Using \u201cTest as add-on\u2026\u201d mode<\/em>\n\nBefore you publish the Script as a web add-on for anyone to use (public access), you can deploy it privately. In order to do this, you need to have your own custom Google Suite and create a copy of your add-on Script (to have a separate \u201cscriptld\u201d), then click \u201cPublish\u201d =&gt; \u201cDeploy as web add-on\u2026\u201d in your script console. After filling in the required fields, you will be redirected to an admin\u2019s web store page of the add-on.\n\n<strong>Note:<\/strong> You need to create a copy of the add-on in order for the Google store to treat it as a separate app. Using clasp locally, as described earlier, you can work on only one project, but still, have the ability to move it either to the production or staging environment (a copy of your add-on).\n\nAt the bottom of the page, you will have the list of visibility options to choose from: \u201cPublic\u201d, \u201cUnlisted\u201d or \u201cPrivate\u201d. The \u201cPublic\u201d checkbox will be enabled by default.\n\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-medium wp-image-10478\" src=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/visibility-options-google-apps-add-on-360x77.png\" alt=\"\" width=\"480\" height=\"102\" srcset=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/visibility-options-google-apps-add-on-360x77.png 360w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/visibility-options-google-apps-add-on-768x163.png 768w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/visibility-options-google-apps-add-on.png 785w\" sizes=\"auto, (max-width: 480px) 100vw, 480px\" \/>\n\n<em style=\"display: block; text-align: center; font-size: 80%;\">Pic. 2. Visibility options of Google Apps Script add-on<\/em>\n\nAfter starting to use the staging environment, it would be good to introduce some environmental variables that will help distinguish the environment itself from the one within the add-on. It would be helpful, for example, to add additional functionality to the staging environment for developers to be able to test some specific features.\n\nTo return certain environment, you can either use a custom function in the back end called ADDON_ENV() or dig deeper and use .env file. It will allow you to automate the process of pushing source to different projects with different source ID by using a webpacker or similar tool.\n<h3>5. Google Apps Script Triggers<\/h3>\n<h4>Triggers frequency<\/h4>\nUsing time-based triggers in an add-on is similar to a cron task that runs by rules set up for it. You can schedule the times when you want the triggers to run. You may configure them to run infinitely with a certain frequency. However, you cannot set up a trigger with a time interval of less than one hour. In other words, the maximum frequency is once per hour.\n\nThe only way one can make a Google Apps Script trigger run every minute is by configuring it manually in the development environment. To do this, go to <a href=\"https:\/\/script.google.com\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">https:\/\/script.google.com<\/a>, open the add-on you want to add a trigger to, click \u201cEdit\u201d =&gt; \u201cCurrent project\u2019s triggers\u201d. You will be able to see the list of all your <a href=\"https:\/\/stackoverflow.com\/questions\/29014087\/apps-script-private-functions\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">public functions<\/a>. Here you can manage your triggers and give them a one-minute frequency. This is the only way that you can test triggers locally.\n<h4>Development restrictions<\/h4>\nAre you familiar with this kind of error message?\n\n<i style=\"font-size: 0.8em;\">&#8220;The Add-on attempted an action that is not permitted in Test as Add-on mode. To use this action, you must deploy the Add-on.&#8221;<\/i>\n\nIt appears when you try to run a trigger in the development environment. Google does not allow that. The only way to accomplish this is to write a wrapper to create a Google Apps Script trigger like this one:\n<pre class=\"lang:javascript\">function ticTac(){\n    \/\/ do whatever you want\n}\n\nfunction createHourlyTrigger(){\n  if(ADDON_ENV() !== 'development'){\n    return ScriptApp.newTrigger('ticTac').timeBased().everyHours(1).create();\n  }\n}\n<\/pre>\nWhat do these functions do? You will not be able to create a trigger if your add-on is in the development environment, meaning that it is not deployed yet. If you are testing a trigger in the development environment, just follow the steps described above and create a manual Google Apps Script trigger by setting up any frequency you want. And for the development environment, you can create the same trigger for function <code>ticTac()<\/code> as was described above.\n<h4>Limitations<\/h4>\nThere are certain quotas and limitations that Apps Script services impose on triggers. You can learn more about them in this <a href=\"https:\/\/developers.google.com\/apps-script\/guides\/services\/quotas\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Quotas for Google Services<\/a> document. As you can see, one can create up to 20 triggers per user, not more. So, you should be careful not to exceed this limit. In addition, each trigger cannot run for more than 6 minutes. Otherwise, you will get a timeout error. This problem and one of the solutions were already described in our <a href=\"https:\/\/railsware.com\/blog\/useful-google-spreadsheet-script-triggers\/\" target=\"_blank\" rel=\"noopener noreferrer\">previous blog post<\/a>. In this blog post, I wanted to cover another aspect of this problem and show how you can avoid reaching 20 triggers per user. If your add-on does not require triggers to run some long-term tasks that may reach a limit of 6 minutes, you can do all your work inside one trigger. Just create that trigger in an onOpen() callback and that\u2019s it.\n\nHere is a Google Apps Script trigger example for this case:\n<pre class=\"lang:javascript\">function onOpen(event) {\n    initHourlyTrigger();\n    \/\/ another stuff you want to do\n}\n\nfunction ticTac(){\n    \/\/ Things you want to run hourly just put here\n    \n    var currentDate = new Date();\n    if(currentDate.getHours() === 0){ \/* do whatever you want once a day *\/ };\n    if(currentDate.getHours() % 3 === 0){ \/* run each 3 hours *\/ }; \n}\n\nfunction initHourlyTrigger(){\n  return getHourlyTrigger() || createHourlyTrigger()\n}\n\nfunction getHourlyTrigger(){\n  return getTriggerByName_('ticTac');\n}\n\nfunction createHourlyTrigger(){\n  if(ADDON_ENV() !== 'development'){\n    return ScriptApp.newTrigger('ticTac').timeBased().everyHours(1).create();\n  }\n}\n\nfunction getTriggerByName_(name){\n  var trigger = null;\n  var ss = SpreadsheetApp.getActiveSpreadsheet();\n  ScriptApp.getUserTriggers(ss).forEach(function(t) {\n    if (t.getHandlerFunction() === name) trigger = t;\n  });\n  return trigger\n}\n<\/pre>\n<h4>How to rename an already running trigger<\/h4>\nWhile I followed the approach described above, I faced another problem. It is not crucial, but I would still like to mention it in this post. What if you want to give the current trigger a new name? There is one small advantage of using the \u201cone trigger\u201d approach: one can change the function in which a trigger works on. Please note that you don\u2019t have to rename the function directly.\n\nSee the solution below:\n<pre class=\"lang:javascript\">function ticTac(){\n    \/\/ you can call new function as well or you have just left this function blank for backwards compatibility\n    newFunctionInsteadOfTicTac();\n}\nfunction newFunctionInsteadOfTicTac(){\n    \/\/ put your code here\n}\n\nfunction getHourlyTrigger(){\n  return getTriggerByName_('newFunctionInsteadOfTicTac');\n}\n\nfunction createHourlyTrigger(){\n  \/\/ delete old one import trigger for backwards compatibility\n  deleteTriggerByName('ticTac');\n  if(ADDON_ENV() !== 'development'){\n    return ScriptApp.newTrigger('newFunctionInsteadOfTicTac').timeBased().everyHours(1).create();\n  }\n}\n\nfunction deleteTriggerByName(name){\n  var trigger = getTriggerByName_(name);\n  if (trigger) ScriptApp.deleteTrigger(trigger);\n}\n<\/pre>\nAfter deploying your code to production, the old function in the trigger will be successfully replaced with a new one.\n<h3>6. Working with add-on menu<\/h3>\nWhile developing the add-on, we faced another problem related to creating menu items dynamically. In our case, we had to create one submenu item per sheet that would be connected to Airtable by our add-on. A click on a certain menu item should trigger re-import of all the data from a remote Airtable view. The problem was that the Google Apps Script API did not allow us to create menu items dynamically.\n\nHere is how the function looks (<a href=\"https:\/\/developers.google.com\/apps-script\/reference\/base\/menu#addItem(String,String)\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Google Apps Script Class Menu<\/a> document):\n<pre class=\"lang:javascript\">function onOpen(event) {\n    var ui = SpreadsheetApp.getUi();\n    var menu = ui.createAddonMenu();\n    menu.addItem(\"Open sidebar\", \"onOpenSidebar\");\n    menu.addToUi();\n}\n\nfunction onOpenSidebar() {\n  console.log(\"Sidebar opened!\");\n}\n<\/pre>\nThe function <code>addItem()<\/code> requires two string arguments. The first argument is a menu item label, and the second one &#8211; is the name of the function that is being called by an onclick event represented as a string as well. It becomes obvious that you cannot pass arguments at all. Only the function name. Of course, you can go through all your Google Apps Script sheets and create menu items dynamically which would include the labels and function names you need. As a result, you will be able to create menu items with dynamic function names. But what about dynamic functions themselves?\n\nSo, the solution is pretty tricky:\n<pre class=\"lang:javascript\">function onOpen(event) {\n    var ui = SpreadsheetApp.getUi();\n    var menu = ui.createAddonMenu();\n    menu.addItem(\"Open sidebar\", \"onOpenSidebar\");\n\n    if (event &amp;&amp; event.authMode === ScriptApp.AuthMode.NONE) {\n        \/\/ There is no ability to either fetch cache or get imported sheets for not authorized user (not enabled add-on). \n        \/\/ Not enough Google Apps Script Permissions.\n    } else {\n        var sheetsMenu = ui.createMenu(\"Re-Import sheet\");\n        getImportedSheets().forEach(function(sheet){\n            sheetsMenu.addItem(sheet.getName(), 'reImportSpreadsheet_' + sheet.getSheetId());\n        });\n        menu.addSubMenu(sheetsMenu);\n    }\n    menu.addToUi();\n}\n\n\/\/ global scope function to assign parametrized functions for imported sheets submenu\n(function (globalScope) {\n    var importedSheetIds = getImportedSheetIds();\n    if (importedSheetIds.length !== 0) {\n        importedSheetIds.forEach(function(sheetId){\n            createMenuFunction(sheetId);\n        });\n    }\n\n    function createMenuFunction(sheetId) {\n        globalScope['reImportSpreadsheet_' + sheetId] = function() {\n            reImportSpreadsheet_(sheetId);\n        }\n    }\n})(this);\n\nfunction reImportSpreadsheet_(sheetId) {\n    \/\/ Exactly callback for user click on certain menu item\n}\n<\/pre>\nAs you can see we are doing manipulation with the global scope of the add-on. That\u2019s why you need to be careful because a piece of code like this one\n<pre class=\"lang:javascript\">(function (globalScope) {\n    console.log('global scope');\n})(this);\n<\/pre>\n&#8230; may spam logs infinitely when your add-on is running. So, if you open StackDriver you will be able to see the output of Google Apps Script logger every 0.5 or 1 second as follows:\n\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-medium wp-image-10484\" src=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/google-apps-script-log-360x197.png\" alt=\"\" width=\"360\" height=\"197\" srcset=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/google-apps-script-log-360x197.png 360w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/google-apps-script-log.png 365w\" sizes=\"auto, (max-width: 360px) 100vw, 360px\" \/>\n\n<em style=\"display: block; text-align: center; font-size: 80%;\">Pic. 3. Google Apps Script Stack Driver logs output<\/em>\n\nSo, be careful when experimenting with the global scope. When you are working with it, don\u2019t forget about Google Apps Script\u2019s limitations. For example, do not get properties right from the global scope because you have a limit of about <a href=\"https:\/\/developers.google.com\/apps-script\/guides\/services\/quotas\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">50K get\/write properties per day<\/a>. This limit is quite reasonable, but it still can be reached. You can try using <a href=\"https:\/\/developers.google.com\/apps-script\/reference\/cache\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">cache services<\/a> which will help you avoid exceeding the Google Apps Script quotas.\n\nThe above example confirms that there is no such concept as Google Apps Script script global variables or even constants. All you have to do is to create a function like this:\n<pre class=\"lang:javascript\">function constants(){\n    return {\n        dailyQuotaLimit: 10,\n        \/\/ and so on\n    }\n}\n<\/pre>\n&#8230; and call it whenever you want. So, this function can serve as a store for all your constants without the need to call properties.\n<h3>7. Google Apps Script Quotas and limitations<\/h3>\nBefore you start coding an add-on, I would recommend taking a look at Google\u2019s document about <a href=\"https:\/\/developers.google.com\/apps-script\/guides\/services\/quotas\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Quotas for Google Services<\/a>. It is also good to know that there is no Google Apps Script API that can provide you with information about already used quotas. However, you can find a way to track an increase in the sheet\u2019s cell value. If you still choose to use <a href=\"https:\/\/developers.google.com\/apps-script\/reference\/properties\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Properties Service<\/a>, you will be able to write more data to one property key as a stringified JSON instead of just using one value per key. This approach will save you a lot of get\/set property calls. Note that you can push up to 9KB of data into one property. Here is the link to a related <a href=\"https:\/\/stackoverflow.com\/questions\/41397711\/what-is-the-maximum-properties-size-in-google-apps-script\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">topic<\/a> on StackOverflow. Have a look at what we have done to store all data of a user&#8217;s Google Apps Script sheets into one property key:\n<pre class=\"lang:javascript\">\/\/ will return JSON as well\nfunction getStoredData(sheet) {\n    sheet = sheet || SpreadsheetApp.getActiveSheet();\n    var value = documentProps().getProperty(sheet.getSheetId());\n    try {\n        \/\/ return blank JSON by default\n        return JSON.parse(value) || {};\n    } catch (e) {\n        return defaultData_();\n    }\n}\n\nfunction setStoredData(sheet, data) {\n    var value = jsonStringify_(data);\n    documentProps().setProperty(sheet.getSheetId(), value);\n    return data;\n}\n\nfunction updateStoredData(sheet, props) {\n    var data = getStoredData(sheet);\n    for (var key in props) {\n        data[key] = props[key];\n    }\n    setStoredData(sheet, data);\n    return data;\n}\n\nfunction deleteStoredData(sheet) {\n    var sheet = sheet || SpreadsheetApp.getActiveSheet();\n    documentProps().deleteProperty(sheet.getSheetId());\n}\n\nfunction documentProps() {\n    return PropertiesService.getDocumentProperties();\n}\n\nfunction jsonStringify_(obj) {\n    return JSON.stringify(obj, Object.keys(obj || {}).sort());\n}\n<\/pre>\nSo, as you can see in the example above, you can simply put a certain JSON instead of a string into one property.\n<h3>8. Google Add-on Payment Integration<\/h3>\nAt the moment, Google does not provide options for add-ons to charge users yet. However, it allows vendors to integrate one of the existing payment systems or build their own for this purpose.\n\nHere is what Google says about charging add-on users in the <a href=\"https:\/\/developers.google.com\/apps-script\/add-ons\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Develop Add-ons for Google Sheets, Docs, Slides, Forms, and Gmail<\/a> document:\n\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-10498\" src=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/google-docs-cant-include-ads-1.png\" alt=\"How to charge users for Google Add-ons\" width=\"865\" height=\"105\" srcset=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/google-docs-cant-include-ads-1.png 865w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/google-docs-cant-include-ads-1-360x44.png 360w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/google-docs-cant-include-ads-1-768x93.png 768w\" sizes=\"auto, (max-width: 865px) 100vw, 865px\" \/>\n<em style=\"display: block; text-align: center; font-size: 80%;\">Pic. 4. Charging Google add-on users<\/em>\n\nSo, do not hesitate to integrate the payment system you want. In our case, we have explored a few solutions and opted for ChargeBee. This system provides simple integration and payment\/subscription validation, as well as drop-in Google Apps Script UI. In addition, it takes over all payment-related user management for us.\n<h3>Epilogue: useful tidbits<\/h3>\n<ul>\n \t<li>Do not hesitate to use the <a href=\"https:\/\/developers.google.com\/apps-script\/reference\/cache\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">cache service<\/a> since it can increase the loading speed and save the Google Apps Script quotas like crazy.<\/li>\n \t<li>Use <a href=\"https:\/\/stackoverflow.com\/questions\/29014087\/apps-script-private-functions\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">private functions<\/a> that end with an underscore.<\/li>\n \t<li>Remember that sheet names are unique. You can\u2019t create 2 sheets with the same name inside one spreadsheet.<\/li>\n \t<li>The <a href=\"https:\/\/developers.google.com\/apps-script\/reference\/url-fetch\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">URL Fetch Service<\/a> makes requests from the server side, so you should not worry about the CORS.<\/li>\n \t<li>There is no notification or a webhook call to let you know that a new version of an add-on was successfully published to production. Show the version number somewhere in the Google Apps Script UI to track version update progress.<\/li>\n \t<li>You probably realized that Google Apps Script does not support ES6, pure JS only. In all examples above there were no ES6. But, of course, if you start using <a href=\"https:\/\/github.com\/google\/clasp\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">clasp<\/a> you can simply use <a href=\"https:\/\/babeljs.io\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">babel<\/a> locally to transform all your .js files with ES6 to .gs files processable by Google Apps Script.<\/li>\n<\/ul>\nI hope that this blog post was useful and helped you overcome some issues, emphasized the importance of some aspects over others, opened up your eyes to details, and just generally saved lots of your time!\n\nHappy Coding! :)","protected":false},"excerpt":{"rendered":"<p>Read this blog post to find out the best practices our team applied when using Google Apps Script to build the Airtable Importer Google Sheets add-on.<\/p>\n","protected":false},"author":74,"featured_media":10493,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[3,364],"tags":[],"coauthors":["Ruslan Kuzma"],"class_list":["post-10439","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development","category-labs"],"acf":[],"aioseo_notices":[],"categories_data":[{"name":"Engineering","link":"https:\/\/railsware.com\/blog?category=development"},{"name":"Labs","link":"https:\/\/railsware.com\/blog?category=labs"}],"post_thumbnails":"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2018\/08\/GASGotchas-illustration.jpg","amp_enabled":true,"_links":{"self":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/10439","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/users\/74"}],"replies":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/comments?post=10439"}],"version-history":[{"count":72,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/10439\/revisions"}],"predecessor-version":[{"id":15241,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/10439\/revisions\/15241"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/media\/10493"}],"wp:attachment":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/media?parent=10439"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/categories?post=10439"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/tags?post=10439"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/coauthors?post=10439"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}