Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(node): Add LocalVariables integration to capture local variables to stack frames #6478

Merged
merged 24 commits into from Jan 9, 2023

Conversation

timfish
Copy link
Collaborator

@timfish timfish commented Dec 9, 2022

Closes #6369

Takes the demo by @kamilogorek and adds:

  • Caching with LRU + hash so that the correct previously encountered exception can be looked up
  • Adds local variables for as many frame as possible
  • Fixes a couple of minor issues on older versions of node

It looks like the error.stack is available through the inspector error data.description. This is parsed and used to create a unique hash which is used later to lookup vars for stack frames when the event processor is run.

Applying vars to frames is best effort since the function names from the inspector don't always match those from the stack trace. The most obvious place this happens it anonymous functions. For example the name of anonymous function passed to setTimeout shows as an empty string from the inspector and Timeout._onTimeout in the stack trace.

What's left:

  • Add e2e Tests
  • Looks like it'll also just work for reason: 'promiseRejection'
  • Could probably do with better comparison for anonymous functions

@AbhiPrasad
Copy link
Member

Do we have an idea of the performance impact here?

@AbhiPrasad AbhiPrasad self-requested a review December 9, 2022 13:48
@timfish
Copy link
Collaborator Author

timfish commented Dec 9, 2022

I haven't measured anything yet but I suspect it's Not That Bad™️.

I've had to cache the promise(s) looking up the stack vars because I found that the event processor was called before all the inspector async calls returned. This suggests that the debug pause isn't that long. I guessed it would block until our async function completed but it doesn't.

I'm wondering whether the inspector API might be a suitable alternative to hooking error and unhandledRection since the stack frames look better than what we can get from parsing error.stack, especially when it comes to async frames.

One caveat: I've only tested a single recent version of node!

@timfish
Copy link
Collaborator Author

timfish commented Dec 12, 2022

This is now failing the AWS Lambda serverless build. I had assumed that dynamicRequire would negate all bundler issues.

What's the safest way to only load this integration in node.js >= v14 while not tripping up Rollup or other bundlers?

@AbhiPrasad
Copy link
Member

What's the safest way to only load this integration in node.js >= v14 while not tripping up Rollup or other bundlers?

Can we just always import this integration but then gate the functionality by a runtime node version check?

@timfish
Copy link
Collaborator Author

timfish commented Dec 12, 2022

Can we just always import this integration but then gate the functionality by a runtime node version check?

My concern was ending up with code containing require('inspector') which would fail on runtimes that don't have that module. We would need to gate that import or gate importing the entire integration to avoid that. I've just checked and node.js v8 does support it (although it's experimental) so it shouldn't be an issue with the node.

However, will require('inspector') result in an error on AWS lambda?

packages/node/src/types.ts Outdated Show resolved Hide resolved
@AbhiPrasad
Copy link
Member

However, will require('inspector') result in an error on AWS lambda?

It shouldn't since it's running NodeJS, so the APIs should all be there, and we're not worried about non-node platforms for now. If the import exists since Node 8, we should be good to go.


const testScriptPath = path.resolve(__dirname, 'no-local-variables.js');

childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => {

Check warning

Code scanning / CodeQL

Shell command built from environment values

This shell command depends on an uncontrolled [absolute path](1).

const testScriptPath = path.resolve(__dirname, 'local-variables.js');

childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => {

Check warning

Code scanning / CodeQL

Shell command built from environment values

This shell command depends on an uncontrolled [absolute path](1).
@timfish timfish marked this pull request as ready for review December 13, 2022 13:40
@timfish
Copy link
Collaborator Author

timfish commented Dec 13, 2022

This is ready for a first pass!

Copy link
Member

@AbhiPrasad AbhiPrasad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1st pass. Would be nice to get some unit tests for the event manipulation and _handlePaused func. Thanks for taking a look Tim!

packages/node/src/integrations/localvariables.ts Outdated Show resolved Hide resolved
packages/node/src/integrations/localvariables.ts Outdated Show resolved Hide resolved
packages/node/src/integrations/localvariables.ts Outdated Show resolved Hide resolved
packages/node/src/integrations/localvariables.ts Outdated Show resolved Hide resolved
packages/node/src/integrations/localvariables.ts Outdated Show resolved Hide resolved
@smeubank
Copy link
Member

the option is off by default correct? Did you already start looking at adding it to docs for node?

@timfish
Copy link
Collaborator Author

timfish commented Dec 15, 2022

Yes, this is disabled by default and enabled by options._experiments.includeStackLocals.

It's in _experiments because if this becomes enabled by default, it'll be disabled by removing the integration rather than having it's own specific option.

Did you already start looking at adding it to docs for node?

No but I will do!

@timfish timfish self-assigned this Dec 20, 2022
Copy link
Member

@AbhiPrasad AbhiPrasad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's :shipit: - excited to see this out there.

We also need a docs PR for this!

this.on('Debugger.paused', onPause);
this.post('Debugger.enable');
// We only want to pause on uncaught exceptions
this.post('Debugger.setPauseOnExceptions', { state: 'uncaught' });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean we only add stack locals for uncaught exceptions?

What do you think about making this configurable then?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's probably worth adding an options object as the first constructor argument so we can also add more options in the future without a breaking change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add that now, and then merge this in? We can keep the options object empty for now.

stackParser: StackParser,
{ params: { reason, data, callFrames } }: InspectorNotification<PausedExceptionEvent>,
): Promise<void> {
if (reason !== 'exception' && reason !== 'promiseRejection') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a list of all the reasons somewhere?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://chromedevtools.github.io/devtools-protocol/v8/Debugger/#event-paused

Allowed Values: ambiguous, assert, CSPViolation, debugCommand, DOM, EventListener, exception, instrumentation, OOM, other, promiseRejection, XHR

@timfish
Copy link
Collaborator Author

timfish commented Jan 9, 2023

Added an empty options object.

Docs PR next!

@AbhiPrasad AbhiPrasad merged commit 57c7e7b into getsentry:master Jan 9, 2023
@timfish timfish deleted the feat/stack-locals branch January 9, 2023 12:49
shrzaf added a commit to digitalservicebund/grundsteuer that referenced this pull request Jan 16, 2023
Even though this could be pretty useful, local variables may contain sensitive information and sentry seems to be planning to add this to the list of default integrations. See getsentry/sentry-javascript#6478
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[node] Stack locals
4 participants