Let’s say you need to find out how your function was called, either for in app debugging or some other purpose. A JavaScript Error has a non standard property (stack) that contains some info about how to find out. This is supported by V8, so since Apps Script is running V8 we can use it.
Motivation
It’s not a great idea to modify a functions behavior depending on who called it, but for Sheets Workbook functions – converted to Apps Script I needed to find a way of handling the Sheets function Arrayformula, which modifies the behavior of other functions when used as a function from sheets. I already have a way of selecting a default behavior for a function as described in JavaScript snippet to create a default method for an object but I also need to know whether it’s under the control of Arrayformula or not. This is not great practice, but it’s necessary to be able to exactly emulate how Arrayformula modifies the behavior of the function it’s controlling.
The stack
For demo purposes. I’ll use this simple stack structure. The objective here is that in function d, I want to find out who called me.
function a() {
b()
function b() {
c()
}
}
function c() {
d()
}
function d() {
const stacked = stacker()
console.log('got called like this', stacked)
console.log('immediate caller was', stacked[0].name)
}
demo
Creating a stack
It can be extracted from an new Error instance, as below. You can see that everything we need is there, so we’ll just need a few regexes to clean it up
// check we're running in supported v8 environment
if (!Error.captureStackTrace) {
throw new Error('stack trace not supported - only works on V8')
}
// create an instance of an error
const instance = new Error();
// the stack trace looks like this
/*
Error
at stacker (test:109:20)
at d (test:96:19)
at c (test:92:3)
at b (test:88:5)
at a (test:85:3)
at __GS_INTERNAL_top_function_call__.gs:1:8
*/
const { stack } = instance
stack trace
Anonymous functions
Anonymous functions have a slightly different trace format so we’re going to need to cater for that too. Consider this version of the c function in the test
function c() {
(()=>d())()
}
anonymous function
Anonymous stack trace
Error
at stacker (test:109:20)
at d (test:96:19)
at test:92:8
at c (test:92:12)
at b (test:88:5)
at a (test:85:3)
at __GS_INTERNAL_top_function_call__.gs:1:8
anonymous stack trace
Object methods
These have a slightly different format too
function a() {
const o = {
b: ()=> c()
}
o.b()
}
function c() {
(()=>d())()
}
function d() {
const stacked = stacker()
console.log('got called like this', stacked)
console.log('this is me', stacked[0])
console.log('this is who called me', stacked[1])
}
called from an object method
Object method stack trace
Error
at stacker (test:109:20)
at d (test:95:19)
at test:91:8
at c (test:91:12)
at Object.b (test:86:13)
at a (test:88:5)
at __GS_INTERNAL_top_function_call__.gs:1:8
object method stack trace
Property get and set
And again, a little different
function a() {
const o = {
get b (){
return c()
}
}
o.b()
}
function c() {
(()=>d())()
}
function d() {
const stacked = stacker()
console.log('got called like this', stacked)
console.log('this is me', stacked[0])
console.log('this is who called me', stacked[1])
}
property get
Property get stack trace. A property set gives the same trace, except Object.set rather than Object.get
Info Error
at stacker (test:111:20)
at d (test:97:19)
at test:93:8
at c (test:93:12)
at Object.get b [as b] (test:87:14)
at a (test:90:5)
at __GS_INTERNAL_top_function_call__.gs:1:8
property get stack trace
Instantiated class
Similar to as Object method and property set and get except this time you get the class name rather than ‘Object’
Cleaning up the stack
We need a regex to cater for this variety of stack traces, preferably one that deconstructs into each of the variants above, which can be expressed as
\n at [[object|class.name[.get|.set]][name] [as othername] [(]file:line:position][)]
I’d like to organize that into an object like this.
{
name, // the caller name or 'anonymous'
className, // the class name or 'Object'
type, // get, set or
file, // the script file
line, // the line number in the script file
position, // the position in the line in the script file
depth // the depth where the function that calls stacker is 0
}
result structure
It’s a pretty unstructured thing, so we have to get a bit hacky with the regexs, as below
// the file info
const fx = /\(?(\w ):(\w ):(\w )\)?$/
// to get rid of the file info
const dx = /\s (\(?\w :\w :\w \)?)$/
// tp split the remainder
const sx = /\s*?at\s (\w )?[\s\.]*(\w )?[\s\.]*(\w )?/
stacker regexes
The result
The result is an array of information about the callers. Since this is going to be a generalized function (stacker) , we will slice off the call to the stacker itself as well as the Apps Script run time initialization – none of which are of interest
[{
"name": "d",
"className": null,
"type": null,
"file": "test",
"line": 103,
"position": 19,
"depth": 0
}, {
"name": null,
"className": null,
"type": null,
"file": "test",
"line": 99,
"position": 8,
"depth": 1
}, {
"name": "c",
"className": null,
"type": null,
"file": "test",
"line": 99,
"position": 12,
"depth": 2
}, {
"name": "m",
"className": "E",
"type": null,
"file": "test",
"line": 89,
"position": 12,
"depth": 3
}, {
"name": "a",
"className": null,
"type": null,
"file": "test",
"line": 95,
"position": 12,
"depth": 4
}]
result
Stacker function
Now this can be called from anywhere to reveal who the caller is.
const stacker = () => {
// check we're running in supported v8 environment
if (!Error.captureStackTrace) {
throw new Error('stack trace not supported - only works on V8')
}
// create an instance of an error
const instance = new Error();
const { stack } = instance
// dump the call from apps script rt and the Error introduction
// we should end up with a an array of object names, callers and files and line numbers
// best to do the regex in bits
// the file info
const fx = /\(?(\w ):(\w ):(\w )\)?$/
// to get rid of the file info
const dx = /\s (\(?\w :\w :\w \)?)$/
// tp split the remainder
const sx = /\s*?at\s (\w )?[\s\.]*(\w )?[\s\.]*(\w )?/
return stack.split("\n")
.slice(2, -1)
.map((m,i) => {
// the file info
const f = m.match(fx)
// the remainder
const d = m.replace(dx,'')
// sort out the caller object name etc.
const s = d.match(sx)
// if none, then its anonymous
// if 1 its a straight function
// if 2 then its something like object.name
// if we have all 3 then its something like object.get name
return {
name: (s && (s[3] || s[2] || s[1])) || null,
className: (s && (s[3] || s[2])) ? s[1] : null,
type: s && s[3] ? s[2] : null,
file: f[1],
line: parseInt(f[2]),
position: parseInt(f[3]),
depth: i
}
})
}
stacker
and it can be used like this from any function to clean up the call stack
function d() {
const stacked = stacker()
console.log('got called like this', stacked)
console.log('this is me', stacked[0])
console.log('this is who called me', stacked[1])
}
stacker usage