Skip to content

Commit cc0a5fa

Browse files
authored
Enable depth of traversal for node local vars (#914)
* feat: enable depth of traversal for node local vars * fix: style fixes
1 parent e04e7c5 commit cc0a5fa

File tree

5 files changed

+388
-32
lines changed

5 files changed

+388
-32
lines changed

src/server/locals.js

+104-21
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
/* globals Map */
22
var inspector = require('inspector');
33
var async = require('async');
4+
var _ = require('../utility');
5+
6+
// It's helpful to have default limits, as the data expands quickly in real environments.
7+
// depth = 1 is enough to capture the members of top level objects and arrays.
8+
// maxProperties limits the number of properties captured from non-array objects.
9+
// When this value is too small, relevant values for debugging are easily omitted.
10+
// maxArray applies to array objects, which in practice may be arbitrarily large,
11+
// yet for debugging we usually only care about the pattern of data that is established,
12+
// so a smaller limit is usually sufficient.
13+
var DEFAULT_OPTIONS = {
14+
depth: 1,
15+
maxProperties: 30,
16+
maxArray: 5
17+
}
418

5-
function Locals(config) {
19+
function Locals(options) {
620
if (!(this instanceof Locals)) {
7-
return new Locals(config);
21+
return new Locals(options);
822
}
923

10-
this.config = config;
24+
options = _.isType(options, 'object') ? options : {};
25+
this.options = _.merge(DEFAULT_OPTIONS, options);
1126

1227
this.initSession();
1328
}
@@ -62,7 +77,7 @@ Locals.prototype.mergeLocals = function(localsMap, stack, key, callback) {
6277
return callback(e);
6378
}
6479

65-
getLocalScopesForFrames(matchedFrames, callback);
80+
getLocalScopesForFrames(matchedFrames, this.options, callback);
6681
}
6782

6883
// Finds frames in localParams that match file and line locations in stack.
@@ -115,11 +130,12 @@ function matchedFrame(callFrame, stackLocation) {
115130
callFrameColumn === position.column;
116131
}
117132

118-
function getLocalScopesForFrames(matchedFrames, callback) {
119-
async.each(matchedFrames, getLocalScopeForFrame, callback);
133+
function getLocalScopesForFrames(matchedFrames, options, callback) {
134+
async.each(matchedFrames, getLocalScopeForFrame.bind({ options: options }), callback);
120135
}
121136

122137
function getLocalScopeForFrame(matchedFrame, callback) {
138+
var options = this.options;
123139
var scopes = matchedFrame.callFrame.scopeChain;
124140

125141
var scope = scopes.find(scope => scope.type === 'local');
@@ -135,35 +151,102 @@ function getLocalScopeForFrame(matchedFrame, callback) {
135151

136152
var locals = response.result;
137153
matchedFrame.stackLocation.locals = {};
138-
for (var local of locals) {
139-
matchedFrame.stackLocation.locals[local.name] = getLocalValue(local);
154+
var localsContext = {
155+
localsObject: matchedFrame.stackLocation.locals,
156+
options: options,
157+
depth: options.depth
140158
}
141-
142-
callback(null);
159+
async.each(locals, getLocalValue.bind(localsContext), callback);
143160
});
144161
}
145162

146-
function getLocalValue(local) {
147-
var value;
163+
function getLocalValue(local, callback) {
164+
var localsObject = this.localsObject;
165+
var options = this.options;
166+
var depth = this.depth;
167+
168+
function cb(error, value) {
169+
if (error) {
170+
// Add the relevant data to the error object,
171+
// taking care to preserve the innermost data context.
172+
if (!error.rollbarContext) {
173+
error.rollbarContext = local;
174+
}
175+
return callback(error);
176+
}
148177

149-
switch (local.value.type) {
150-
case 'undefined': value = 'undefined'; break;
151-
case 'object': value = getObjectValue(local); break;
152-
case 'array': value = getObjectValue(local); break;
153-
default: value = local.value.value; break;
178+
if (_.typeName(localsObject) === 'array') {
179+
localsObject.push(value);
180+
} else {
181+
localsObject[local.name] = value;
182+
}
183+
callback(null);
154184
}
155185

156-
return value;
186+
if (!local.value) {
187+
return cb(null, '[unavailable]');
188+
}
189+
190+
switch (local.value.type) {
191+
case 'undefined': cb(null, 'undefined'); break;
192+
case 'object': getObjectValue(local, options, depth, cb); break;
193+
case 'function': cb(null, getObjectType(local)); break;
194+
case 'symbol': cb(null, getSymbolValue(local)); break;
195+
default: cb(null, local.value.value); break;
196+
}
157197
}
158198

159-
function getObjectValue(local) {
199+
function getObjectType(local) {
160200
if (local.value.className) {
161-
return '<' + local.value.className + ' object>'
201+
return '<' + local.value.className + ' object>';
162202
} else {
163-
return '<object>'
203+
return '<object>';
164204
}
165205
}
166206

207+
function getSymbolValue(local) {
208+
return local.value.description;
209+
}
210+
211+
function getObjectValue(local, options, depth, callback) {
212+
if (!local.value.objectId) {
213+
if ('value' in local.value) {
214+
// Treat as immediate value. (Known example is `null`.)
215+
return callback(null, local.value.value);
216+
}
217+
}
218+
219+
if (depth === 0) {
220+
return callback(null, getObjectType(local));
221+
}
222+
223+
getProperties(local.value.objectId, function(err, response){
224+
if (err) {
225+
return callback(err);
226+
}
227+
228+
var isArray = local.value.className === 'Array';
229+
var length = isArray ? options.maxArray : options.maxProperties;
230+
var properties = response.result.slice(0, length);
231+
var localsContext = {
232+
localsObject: isArray ? [] : {},
233+
options: options,
234+
depth: depth - 1
235+
}
236+
237+
// For arrays, use eachSeries to ensure order is preserved.
238+
// Otherwise, use each for faster completion.
239+
var iterator = isArray ? async.eachSeries : async.each;
240+
iterator(properties, getLocalValue.bind(localsContext), function(error){
241+
if (error) {
242+
return callback(error);
243+
}
244+
245+
callback(null, localsContext.localsObject);
246+
});
247+
});
248+
}
249+
167250
function getProperties(objectId, callback) {
168251
Locals.session.post('Runtime.getProperties', { objectId : objectId, ownProperties: true }, callback);
169252
}

src/server/parser.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,9 @@ exports.parseException = function (exc, options, item, callback) {
314314
item.notifier.locals.mergeLocals(item.localsMap, stack, exc.stack, function (err) {
315315
if (err) {
316316
logger.error('could not parse locals, err: ' + err);
317-
return callback(err);
317+
318+
// Don't reject the occurrence, record the error instead.
319+
item.diagnostic['error parsing locals'] = err;
318320
}
319321

320322
return callback(null, ret);

src/server/rollbar.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ function Rollbar(options, client) {
4141
var api = new API(this.options, transport, urllib, truncation, jsonBackup);
4242
var telemeter = new Telemeter(this.options)
4343
this.client = client || new Client(this.options, api, logger, telemeter, 'server');
44-
if (options.locals) {
44+
if (this.options.locals) {
4545
// Capturing stack local variables is only supported in Node 10 and higher.
4646
var nodeMajorVersion = process.versions.node.split('.')[0];
4747
if (nodeMajorVersion >= 10) {
48-
this.locals = new Locals(this.options)
48+
this.locals = new Locals(this.options.locals);
4949
}
5050
}
5151
addTransformsToNotifier(this.client.notifier);

test/fixtures/locals.fixtures.js

+40-3
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ var localsFixtures = {
314314
type: 'object',
315315
className: 'FooClass',
316316
description: 'FooClass',
317-
objectId: '{"injectedScriptId":1,"id":48}'
317+
objectId: 'nestedProps1'
318318
},
319319
writable: true,
320320
configurable: true,
@@ -327,7 +327,7 @@ var localsFixtures = {
327327
type: 'object',
328328
className: 'BarClass',
329329
description: 'BarClass',
330-
objectId: '{"injectedScriptId":1,"id":48}'
330+
objectId: 'nestedProps2'
331331
},
332332
writable: true,
333333
configurable: true,
@@ -370,7 +370,7 @@ var localsFixtures = {
370370
array1: {
371371
name: 'args',
372372
value: {
373-
type: 'array',
373+
type: 'object',
374374
subtype: 'array',
375375
className: 'Array',
376376
description: 'Array(1)',
@@ -380,6 +380,43 @@ var localsFixtures = {
380380
configurable: true,
381381
enumerable: true,
382382
isOwn: true
383+
},
384+
function1: {
385+
name: 'func',
386+
value: {
387+
type: 'function',
388+
className: 'Function',
389+
description: 'Array(1)',
390+
objectId: '{"injectedScriptId":1,"id":64}'
391+
},
392+
writable: true,
393+
configurable: true,
394+
enumerable: true,
395+
isOwn: true
396+
},
397+
function2: {
398+
name: 'asyncFunc',
399+
value: {
400+
type: 'function',
401+
className: 'AsyncFunction',
402+
objectId: '{"injectedScriptId":1,"id":32}'
403+
},
404+
writable: true,
405+
configurable: true,
406+
enumerable: true,
407+
isOwn: true
408+
},
409+
null1: {
410+
name: 'parent',
411+
value: {
412+
subtype: 'null',
413+
type: 'object',
414+
value: null
415+
},
416+
configurable: true,
417+
writable: true,
418+
enumerable: true,
419+
isOwn: true
383420
}
384421
}
385422
}

0 commit comments

Comments
 (0)