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

Enable node.js stack locals capture #902

Merged
merged 6 commits into from
Jan 4, 2021
Merged

Enable node.js stack locals capture #902

merged 6 commits into from
Jan 4, 2021

Conversation

waltjones
Copy link
Contributor

@waltjones waltjones commented Nov 4, 2020

Description of the change

Enables capture of local stack variables in Node.js. To enable, set locals: true in the config.

This feature uses the Node Inspector API to access debugger data at runtime. The Inspector API is a wrapper for the V8 Inspector interface. While the V8 interface relies on send() and receive() functions that can't be accessed from Node, the Node Inspector API provides access to these via the post() method on the Inspector Session instance. Using session.post(), all of the DevTools protocol can be accessed, including the Debugger and Runtime interfaces, which are used here.

The initial version of this feature supports both caught and uncaught exceptions and promise rejections. Nested exceptions and trace chains are supported.

The DevTools protocol allows retrieving nested variables to an arbitrary depth. However, the initial version of this feature only implements the equivalent of depth = 0. A future PR may enable arbitrary depth with a config option to set the depth.

Unexpanded objects are identified by their class, in keeping with the example in the Rollbar API docs. (https://explorer.docs.rollbar.com/#tag/Item)

Example:

trace: {
  frames: [
    {
      // ...
      locals: {
        foo: 'bar', // immediate string value
        obj: '<Foo object>' // instance of Foo
      }
    }
  ]
}

The feature is enabled for Node 10+. Node 8 doesn't implement the url property on the CallFrame object, and cannot reliably resolve to the correct locations in the stack. (There may be a workaround, but none that I know at this time.)

This PR adheres to the established style of Rollbar.js and uses var declarations, rather than let and const. ES5 style classes are used. I'm not sure whether session.post() supports async/await. Regardless, callbacks are used throughout as shared code that cannot be made async prevents the use of async/await in this code path.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Related issues

Fixes: ch74679

Development

  • Lint rules pass locally
  • The code changed/added as part of this pull request has been covered with tests
  • All tests related to the changed code pass in development

Code review

  • This pull request has a descriptive title and information useful to a reviewer. There may be a screenshot or screencast attached
  • "Ready for review" label attached to the PR and reviewers mentioned in a comment
  • Changes have been reviewed by at least one other engineer
  • Issue from task tracker has a link to this pull request

Copy link

@eudaimos eudaimos left a comment

Choose a reason for hiding this comment

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

@waltjones

Overall I made comments/asked questions about the implementation and public api of Locals

Also wondering, should there be unit tests for the Locals API?
The tests I see seem to be routed through the rollbar SDK

var key = params.data.description;
this.currentErrors.set(key, params);

if (this.currentErrors.size > 4) {

Choose a reason for hiding this comment

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

@waltjones what's the significance of the size > 4 - should this be maintained in a descriptive constant value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Constant and explanatory comment added.

return callback(e);
}

this.getLocalScopesForFrames(matchedFrames, function(err) {

Choose a reason for hiding this comment

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

is there a reason not to just pass callback as the second arg here?
as in:

this.getLocalScopesForFrames(matchedFrames, callback);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Each of these cases have been condensed now.

return matchedFrames;
}

Locals.prototype.firstFrame = function(localIndex, stackIndex) {

Choose a reason for hiding this comment

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

is there a need for this function to be attached to the Locals.prototype?

Seems better suited as a private local utility function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Non-interface functions are all private now.

return !localIndex && !stackIndex;
}

Locals.prototype.matchedFrame = function(callFrame, stackLocation) {

Choose a reason for hiding this comment

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

same comment about this function as the above for firstFrame - this function isn't making use of this - does it need to be exported on the Locals.prototype?

}

Locals.prototype.getLocalScopesForFrames = function(matchedFrames, callback) {
async.each(matchedFrames, this.getLocalScopeForFrame.bind(this), function (err) {

Choose a reason for hiding this comment

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

seems that you can pass callback as the thrid arg to async.each rather than having the wrapping function - is there a reason for the extra wrapping function?

seems it will be called no matter the outcome the way it's written

callFrameColumn === position.column;
}

Locals.prototype.getLocalScopesForFrames = function(matchedFrames, callback) {

Choose a reason for hiding this comment

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

does this function need to be attached to the Locals.prototype and exported? I don't see it used outside of the mergeLocals body which does need to be exported (public).

As for the bind call to this, that could be done by mergeLocals when passing maybe? Just a thought on trying to expose less of these internal methods if they're not needed externally

var _this = this;
for (var i = 0; i < scopes.length; i++) {
var scope = scopes[i]
if (scope.type === 'local') {

Choose a reason for hiding this comment

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

why not filter before the loop using a scopes.filter(scope => scope.type === 'local')

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to use find and expect one local scope.

return value;
}

Locals.prototype.getObjectValue = function(local) {

Choose a reason for hiding this comment

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

does this need to be exported as attached to the Locals.prototype?

}
}

Locals.prototype.getLocalValue = function(local) {

Choose a reason for hiding this comment

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

since the only ref to this is for the this.getObjectValue call, does this need to be exported on the Locals.prototype?

var inspector = require('inspector');
var async = require('async');

function Locals(config) {

Choose a reason for hiding this comment

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

defining classes like this the old school JS way has the problem of possible function call without instantiation, i.e. missing the new keyword by just calling Locals(…) as a function. To prevent this from doing harm, a pattern emerged for checking how a constructor is called like this:

function Locals(config) {
  if (!(this instanceof Locals)) {
    return new Locals(config);
  }
  // rest of constructor logic
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.

Copy link

@eudaimos eudaimos left a comment

Choose a reason for hiding this comment

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

I never seem to get this right - requested changes are comments in previous "Comment" and submitting this now as a Review for the process to work

@waltjones
Copy link
Contributor Author

Major updates are:

  • The inspector session is now a singleton at Locals.session. This makes more sense, since multiple Rollbar.js instances should not open multiple sessions. This has the side effect of greatly reducing the need to pass this around since the main need for it was when calling session.post().
  • Unit tests have been added, which allows exercising some conditions not produced when the actual inspector API is called.

@christopherseaman
Copy link

Update?

@eudaimos @waltjones

@waltjones
Copy link
Contributor Author

Just waiting on approval or further feedback.

src/server/locals.js Show resolved Hide resolved
src/server/locals.js Show resolved Hide resolved
src/server/locals.js Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants