Detect what calls your Node.js code
Tags: node, stack, javascript, typescript
Published: 2023-12-11 16:30:02

While most of the time it's actively discouraged to detect and react to how your code is called, there are times when you would really want to know how the execution came around calling it.

Examples when this would be very useful are:
- Logging libraries that autodetect and describe the caller
- Invisible caching for example if you were to reimplement useEffect on the backend

I'm sure you will find more creative ways to utilize this madness. Let's jump in and see how to do it.

## How is this possible

The trick is to utilize V8 stack capturing functionality. No errors are thrown here, just a simple V8 function is being called to get the stack, and then some parsing happens to make the result useful.

## Prepare

Let's first set up a function with following signature: 
```ts
export function getExecutionPath(
  filterDir = process.cwd(),
  limit = 10
)
```

- `filterDir` - will be used to filter out calls from outside your code, for example, from node_modules or linked libraries
- `limit` - how far into the execution history we will reach

## Capture the stack

Now get the most important part, a fake Error stack trace:
```ts
const inobj = { stack: "" } satisfies { stack: string };

const oldLimit = Error.stackTraceLimit;
Error.stackTraceLimit = limit;
Error.captureStackTrace(inobj);
Error.stackTraceLimit = oldLimit;
```

This code sets the stack limit from the function parameter, captures the stack trace at this very point and restores the original limit.

The result is in pretty rough format:
```
Error
    at getExecutionPath (C:\Code\server-side-state\src\state\getExecutionPath.ts:23:9)
    at new SynchronizedStateful (C:\Code\server-side-state\src\state\state.ts:27:33)
    at state (C:\Code\server-side-state\src\state\state.ts:90:24)
    at Index (C:\Code\server-side-state\src\test\Index.tsx:7:20)
    at C:\Code\server-side-state\src\server.ts:18:25
    at Layer.handle [as handle_request] (C:\Code\server-side-state\node_modules\express\lib\router\layer.js:95:5)
    at next (C:\Code\server-side-state\node_modules\express\lib\router\route.js:144:13)
    at Route.dispatch (C:\Code\server-side-state\node_modules\express\lib\router\route.js:114:3)
    at Layer.handle [as handle_request] (C:\Code\server-side-state\node_modules\express\lib\router\layer.js:95:5)
    at C:\Code\server-side-state\node_modules\express\lib\router\index.js:284:15
```

We need to process that to make it useful. 

## Parse the stack

First run some regex on the lines to get only lines starting with `at` and extract the identifier and its location. At the end remove the call of our detection method from the results and  reverse the order.

```ts
const detectorRegex = /at (.+?) \((.+?)\)/;

const stack = inobj.stack
  .split("\n")
  .map((x) => x.trim())
  .filter((x) => x.startsWith("at"))
  .map((x) => x.match(detectorRegex))
  .filter((x) => !!x) as RegExpMatchArray[];
stack.shift();
stack.reverse();
```

Then, transform the array of matches by parsing those strings inside. Get the identifier, and parse the second parameter to get the path, line and column in mre useful form.

```ts
const detections: StackEntry[] = stack.map((x) => {
  const identifier = x[1];
  const pathlinecol = x[2].split(":");
  const column =
    pathlinecol.length > 2 ? parseInt(pathlinecol.pop() ?? "0") : 0;
  const line =
    pathlinecol.length > 1 ? parseInt(pathlinecol.pop() ?? "0") : 0;
  const p = pathlinecol.join(":");
  return { identifier, path: p, line, column };
});
```

After those steps, the `detections` array looks like this:

```ts
[
  {
    identifier: 'Layer.handle [as handle_request]',
    path: 'C:\\Code\\server-side-state\\node_modules\\express\\lib\\router\\layer.js',
    line: 95,
    column: 5
  },
  {
    identifier: 'Route.dispatch',
    path: 'C:\\Code\\server-side-state\\node_modules\\express\\lib\\router\\route.js',
    line: 114,
    column: 3
  },
  {
    identifier: 'next',
    path: 'C:\\Code\\server-side-state\\node_modules\\express\\lib\\router\\route.js',
    line: 144,
    column: 13
  },
  {
    identifier: 'Layer.handle [as handle_request]',
    path: 'C:\\Code\\server-side-state\\node_modules\\express\\lib\\router\\layer.js',
    line: 95,
    column: 5
  },
  {
    identifier: 'Index',
    path: 'C:\\Code\\server-side-state\\src\\test\\Index.tsx',
    line: 9,
    column: 7
  },
  {
    identifier: 'once',
    path: 'C:\\Code\\server-side-state\\src\\state\\once.ts',
    line: 40,
    column: 26
  },
  {
    identifier: 'OnceRunner.produce',
    path: 'C:\\Code\\server-side-state\\src\\state\\once.ts',
    line: 23,
    column: 21
  }
]
```

## Filter the stack

We see we got some calls that are outside our code, let's filter them out and make the paths relative to our base directory:

```ts
const local = detections.filter(
  (x) =>
    x.path.startsWith(path.resolve(filterDir)) &&
    !x.path.includes("node_modules")
);
```

For breadcrumbs, we still need to filter out anonymous functions and some other stuff, but better to put it in a separate variable `named`, because the things inside `local` can be very useful too.

```ts
  const named = local
    .filter((x) => !x.identifier.includes("<anonymous>"))
    .filter((x) => !x.identifier.startsWith("Object."))
    .filter((x) => !x.identifier.startsWith("Module."));
```

## Final results

And now finally make the breadcrumbs string:

```ts
const breadcrumbs = named.map((x) => x.identifier).join("/");
```

And that's it! In this particular example I used, the breadcrumbs will look like this:
`Index/once/OnceRunner.produce`

and the `local` and `named`:
```ts
local: [
  {
    identifier: 'Index',
    path: 'C:\\Code\\server-side-state\\src\\test\\Index.tsx',
    line: 9,
    column: 7
  },
  {
    identifier: 'once',
    path: 'C:\\Code\\server-side-state\\src\\state\\once.ts',
    line: 40,
    column: 26
  },
  {
    identifier: 'OnceRunner.produce',
    path: 'C:\\Code\\server-side-state\\src\\state\\once.ts',
    line: 23,
    column: 33
  }
],
named: [
  {
    identifier: 'Index',
    path: 'C:\\Code\\server-side-state\\src\\test\\Index.tsx',
    line: 9,
    column: 7
  },
  {
    identifier: 'once',
    path: 'C:\\Code\\server-side-state\\src\\state\\once.ts',
    line: 40,
    column: 26
  },
  {
    identifier: 'OnceRunner.produce',
    path: 'C:\\Code\\server-side-state\\src\\state\\once.ts',
    line: 23,
    column: 33
  }
]
```

Cheers and happy coding as always!