1

So, I'm implementing some tracing logic and want to standardize how my spans are created. I want them to be named after the function they're instantiated inside without having to explicitly pass the name of the function every time. Seems like this should be trivial but I can't find a good way to do it. The best way I came up with is pretty terrible:

function getCurrentFunctionName() {
    // Create an error object to capture the stack trace
    const error = new Error('fakeError')

    // Extract the stack trace and split it into individual lines
    const stackTrace = error.stack?.split('\n')

    if(stackTrace) {
        // Get the second line of the stack trace, which contains the function information
        const functionLine = stackTrace[2]

        const regexResult = ((/at\s+([\w.]+)\s+/u).exec(functionLine))

        if(regexResult && regexResult.length >= 2) {
            // Extract the function name from the function line
            return regexResult[1]
        }
    }

    return 'unknown'
}

This seems silly, especially the part where I use regex to get such basic information. It makes even less sense when you consider that the Error constructor clearly has a way to do this exact same thing otherwise it wouldn't be able to construct the error stack.

I know that I could do something like:

function nonStrictGetFunctionName() {
   return nonStrictGetFunctionName.caller.name
}

Which would be ideal but won't work in my codebase because using caller/arguments is forbidden in strict mode.

Is there any other way to do this?

VLAZ
  • 26,331
  • 9
  • 49
  • 67
Supperhero
  • 911
  • 1
  • 7
  • 24
  • Don't use functions then, but (methods in) classes. You can get the class name. For each variant of the function that needs this, extend the base class for it and override the same method, or use some parameters to configure your class. – trincot Aug 29 '23 at 09:33
  • That would mean reafactoring half the code base. – Supperhero Aug 29 '23 at 09:39
  • Yes, it would... Up to you. – trincot Aug 29 '23 at 09:41
  • Build on a house on top of shifting sand and you'll have to change some things about your building... Or, you can try and do the barest minimum to prop up the house for now, I suppose. – VLAZ Aug 29 '23 at 09:44
  • Maybe the OP provides some boiled down example code of how such things are going to be created and where/how said creator functions are getting invoked. – Peter Seliger Aug 29 '23 at 14:57

2 Answers2

1

In case you don't like your implenetation(which is basically okay imho), you can use npm library called stacktrace-js

Here is example code:

const stacktrace = require("stacktrace-js");

function func() {
    console.log(stacktrace.getSync().map(({ functionName }) => functionName));
}

func();

It will return following array:

[
  'func', // function name
  'Object.<anonymous>',
  'Module._compile',
  'Object.Module._extensions..js',
  'Module.load',
  'Function.Module._load',
  'Function.executeUserEntryPoint [as runMain]',
  undefined
]
-1

Using an error's stack isn't a bad idea imho for testing/debugging.
An alternative could be using Proxy's apply hander.
Note that in the snippet I used a global scope to override the declared functions with proxies.
But there are many ways to override. For example you could override exact need functions manually.
Or if you need all functions in the local scope you could somehow parse the current scope's source code and extract function names and use eval to dynamically override the found functions with proxies. As a direction for parsing: Getting All Variables In Scope

function func1(){};
function func2(){};

for(const name of Object.keys(globalThis)){
  try{
    if(typeof globalThis[name] !== 'function'){
      continue;
    }
    window[name] = new Proxy(globalThis[name], {
      apply(fn, thisArg, args){
        console.log(`calling "${name}" with arguments:`, ...args);
        return Reflect.apply(...arguments);
      }
    });
  }catch(_){}
}

func1('test', true);
func2('test2', false);
Alexander Nenashev
  • 8,775
  • 2
  • 6
  • 17