If you’ve been using Apps Script for a while, since back when it was running on the Rhino JavaScript emulator; see (What JavaScript engine is Apps Script running on?), you’ll remember the Execution Transcript. This was a really useful log of every call made to built-in Apps Script services, along with the arguments passed and the result. It was a great tool for debugging.
With V8, that disappeared, so I wondered if there might be a way to intercept calls to Apps Script services to bring back what we lost with the disappearance of the Execution Transcript. And, of course we can! With a minimum of coding, we can fiddle around with any Gas Service call. This article will show you how.
Approach
There were 2 approaches I considered
- A wrapper around every function that did some logging around every function call. I dismissed this because it would be a mammoth task, and would involve some change of structure to use.
- Use Es6 Proxy to trap calls to Apps Script Services and automatically add logging every time a method was invoked. This involves minimal coding and change of structure.
Obviously I picked option 2, and that’s what I’ll cover here.
What is Proxy
It’s a new Global Object that appeared in ES6, and it allows you to intercept the fundamental workings of a JavaScript object to change its behavior. These intercepts are called traps.
For example, accessing a property like this from an object fires off an internal get of the ‘name’ property in the target object
Setting up a proxy
We can use a proxy for our person object, which will allow us to ‘trap’ a get request, and then fiddle with it. This example does nothing other than return person.name, just as the original object would do
Changing get behavior with a proxy
However we could fiddle with the get and return something different
Traps
In this article, the scope is about changing the behavior of Apps Script methods – so I’m only going to cover 2 traps
- get – when a property is accessed – already referenced in the examples above
- apply – when a function is run
However there are a whole bunch of other traps available – for a full list see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
I’ll no doubt return to some of these in future articles
Apply trap
The get trap applies to objects, but we can also change the behavior of functions using the apply trap. Consider this function
For some reason or another, let’s say we want to create a transcript logging every time it’s called and what the result was without changing the actual function. That’s exactly what the apply trap of a Proxy is for.
Reflect
Most of the time, this example will return the correct result
However if the original object contains a get() property, under certain circumstances (which is outside the scope of this article) confusion about ‘this’ will produce the wrong result. You’ll notice that there’s a ‘receiver’ argument to the get trap – it is required to maintain the correct context for ‘this’ in complex objects.
Luckily the Es6 Reflect object can take care of all of that and it takes the same arguments as those received by the get trap. So I recommend always using Reflect to return the contents of a property in a get trap, rather than accessing the property directly – like this
Recreating the Execution transcript
The above gives us all the tools we need to intercept all Apps Script service calls and to make a transcsript of each time they are called.
Here’s an example of the kind of log you’ll be able to make following this tutorial
Spreadsheet examples
You can apply these techniques to any Apps Script objects, but for this example I’m going to set up proxies for
- SpreadsheetApp
- Spreadsheet
- Sheet
- Range
These 4 proxies will all us to log access by all methods for the main spreadsheet objects. If you’ve read other of my tutorials, you’ll be familiar with the contents of the sheet I’ll be using for this demo
Setting up Spreadsheet proxies
So instead of using the native objects (for example SpreadsheetApp), we’ll reference everything through the proxies for those objects
GasProxyHandler
You’ll notice that each proxy uses exactly the same handler, since we’re doing the same thing for each – namely adding some generalized logging any time any of their methods are called
Explanation
Imagine that you called sheet.getName(). 2 traps would be fired
- the get trap for the property – getName – which happens to be a method and would return a function
- the apply trap for the method getName
The trick here is to intercept the get trap and if the value of the property requested is not a function, then simply Reflect back the value of that object property. However, if the value of the property requested is a function, we’ll return a proxy to that function, which has an apply trap to not only run the the originally requested function but also to log details about what happened, and information about how it was called – in other words, the execution transcript.
You’ll notice that I call a couple of other functions here – which are just to decorate the transcript – so you can prettify them as you want. The key thing is to apply the underlying function and the return the result – everything else is just logging decoration
Decoration
timeIt
wraps the execution of a function calculates the elapsed time
logIt
logs the transscript in whatever format suits – this is just a very basic rendering
Example transcripts
Here’s a few examples of the kind of transcripts generated
Limitations
Chaining is not going to log every part of the chain – for example the chained getA1Notation() part is not logged. This is because the returned range from .offset is a plain Range ie. not one that has been proxied.
So if you wanted both parts of the chain to be logged you need to proxy the result of the offset too
Opportunities
If you tweaked the logging to collect stats by function, you’d have an excellent basis for profiling and visualizing your script to look for functions and code that could be optimized, but that’s for another post.
Summary
If you want something to produce a transcript – proxy it and use the proxy.
And that’s it – the execution transcript resurrected with very little coding
- A handier way of accessing Google Drive folders and files from Apps Script
- Apps Script V8: Arraybuffers and Typed arrays
- Apps Script V8: Arraybuffers and Typed arrays, endianness and views
- Apps Script V8: Keystore for global space
- Apps Script V8: Maps and Sets
- Apps Script V8: Multiple script files, classes & namespaces
- Apps Script V8: Provoke server side code from add-ons and htmlservice
- Apps Script V8: Sorting out the call stack to figure out who called
- Apps Script V8: spreading and destructuring
- Apps Script V8: Template literals
- ES6 Symbols – what on earth is all that about ?
- JavaScript V8 Arrow functions, this and that
- JavaScript V8 variable scopes
- Proxy implementation of Apps Script GraphQL Class
- Resuscitating the Apps Script execution transcript – JavaScript Proxy and Reflect to the rescue