Skip to content

Commit

Permalink
feat: support capturing code context from sourcesContent in the sourc…
Browse files Browse the repository at this point in the history
…e map file (#959)
  • Loading branch information
waltjones authored Jun 29, 2021
1 parent 3d164e4 commit 1ee2cc6
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 13 deletions.
7 changes: 7 additions & 0 deletions examples/node-dist/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Node TypeScript distribution without original source

This dir contains the dist files from ../node-typescript, and is suitable
for testing source maps sourcesContent usage.

This dir is currently only intended as support for rollbar.js test integration.
It does not demonstrate the use of Rollbar.js.
13 changes: 13 additions & 0 deletions examples/node-dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/node-dist/index.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 22 additions & 5 deletions src/server/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ function readFileLines(filename, callback) {

/* Older versions of node do not have fs.exists so we implement our own */
function checkFileExists(filename, callback) {
if (stackTrace.sourceContent(filename)) {
return callback(true);
}
if (fs.exists !== undefined) {
fs.exists(filename, callback);
} else {
Expand All @@ -228,17 +231,31 @@ function gatherContexts(frames, callback) {

tempFileCache = {};

function cacheLines(filename, lines) {
// Cache this in a temp cache as well as the LRU cache so that
// we know we will have all of the necessary file contents for
// each filename in tempFileCache.
tempFileCache[filename] = lines;
cache.set(filename, lines);
}

function gatherFileData(filename, callback) {
var sourceContent = stackTrace.sourceContent(filename);
if (sourceContent) {
try {
var lines = sourceContent.split('\n');
cacheLines(filename, lines);
return callback(null);
} catch (err) {
return callback(err);
}
}
readFileLines(filename, function (err, lines) {
if (err) {
return callback(err);
}

// Cache this in a temp cache as well as the LRU cache so that
// we know we will have all of the necessary file contents for
// each filename in tempFileCache.
tempFileCache[filename] = lines;
cache.set(filename, lines);
cacheLines(filename, lines);

return callback(null);
});
Expand Down
20 changes: 20 additions & 0 deletions src/server/sourceMap/stackTrace.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ var fileContentsCache = {};
// Maps a file path to a source map for that file
var sourceMapCache = {};

// Maps a file path to sourcesContent string
var sourcesContentCache = {};

// Regex for detecting source maps
var reSourceMap = /^data:application\/json[^,]+base64,/;

Expand Down Expand Up @@ -111,6 +114,17 @@ function retrieveSourceMap(source) {
};
}

function cacheSourceContent(sourceMap, originalSource, newSource) {
if (sourcesContentCache[newSource]) {
return;
}

// The sourceContentFor lookup needs the original source url as found in the
// map file. However the client lookup in sourcesContentCache will use
// a rewritten form of the url, hence originalSource and newSource.
sourcesContentCache[newSource] = sourceMap.map.sourceContentFor(originalSource, true);
}

exports.mapSourcePosition = function mapSourcePosition(position, diagnostic) {
var sourceMap = sourceMapCache[position.source];
if (!sourceMap) {
Expand Down Expand Up @@ -153,11 +167,17 @@ exports.mapSourcePosition = function mapSourcePosition(position, diagnostic) {
// better to give a precise location in the compiled file than a vague
// location in the original file.
if (originalPosition.source !== null) {
var originalSource = originalPosition.source;
originalPosition.source = supportRelativeURL(
sourceMap.url, originalPosition.source);
cacheSourceContent(sourceMap, originalSource, originalPosition.source);
return originalPosition;
}
}

return position;
}

exports.sourceContent = function sourceContent(source) {
return sourcesContentCache[source];
}
71 changes: 63 additions & 8 deletions test/server.transforms.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ async function wait(ms) {
});
}

async function throwInTypescriptFile(rollbar, callback) {
async function throwInScriptFile(rollbar, filepath, callback) {
setTimeout(function () {
var error = require('../examples/node-typescript/dist/index');
var error = require(filepath);
error();
}, 10);
await wait(500);
Expand Down Expand Up @@ -256,8 +256,8 @@ vows.describe('transforms')
}
})
.addBatch({
'handleItemWithError': {
'nodeSourceMaps': {
'nodeSourceMaps': {
'with original source present': {
topic: function() {
var Rollbar = new rollbar({
accessToken: 'abc123',
Expand All @@ -267,9 +267,10 @@ vows.describe('transforms')
var queue = Rollbar.client.notifier.queue;
Rollbar.addItemStub = sinon.stub(queue, 'addItem');

throwInTypescriptFile(Rollbar, this.callback);
throwInScriptFile(Rollbar, '../examples/node-typescript/dist/index',
this.callback);
},
'should map the stack': function(r) {
'should map the stack with context': function(r) {
var addItem = r.addItemStub;

assert.isTrue(addItem.called);
Expand All @@ -279,8 +280,15 @@ vows.describe('transforms')
assert.equal(frame.lineno, 10);
assert.equal(frame.colno, 22);
assert.equal(frame.code, " var error = <Error> new CustomError('foo');");
assert.equal(frame.context.pre[0], ' }');
assert.equal(frame.context.pre[1], ' }');
assert.equal(frame.context.pre[2],
' // TypeScript code snippet will include `<Error>`');
assert.equal(frame.context.post[0], ' throw error;');
assert.equal(frame.context.post[1], '}');

var sourceMappingURLs = addItem.getCall(0).args[0].notifier.diagnostic.node_source_maps.source_mapping_urls;
var sourceMappingURLs = addItem.getCall(0).args[0].notifier.diagnostic
.node_source_maps.source_mapping_urls;
var urls = Object.keys(sourceMappingURLs);
assert.ok(urls[0].includes('index.js'));
assert.ok(sourceMappingURLs[urls[0]].includes('index.js.map'));
Expand All @@ -295,8 +303,55 @@ vows.describe('transforms')
assert.ok(sourceMappingURLs[urls[2]].includes('not found'));
}
addItem.reset();
}
}
}
})
.addBatch({
'nodeSourceMaps': {
'using sourcesContent': {
topic: function() {
var Rollbar = new rollbar({
accessToken: 'abc123',
captureUncaught: true,
nodeSourceMaps: true
});
var queue = Rollbar.client.notifier.queue;
Rollbar.addItemStub = sinon.stub(queue, 'addItem');

throwInScriptFile(Rollbar, '../examples/node-dist/index', this.callback);
},
},
'should map the stack with context': function(r) {
var addItem = r.addItemStub;

assert.isTrue(addItem.called);
if (addItem.called) {
var frame = addItem.getCall(0).args[0].body.trace_chain[0].frames.pop();
assert.ok(frame.filename.includes('src/index.ts'));
assert.equal(frame.lineno, 10);
assert.equal(frame.colno, 22);
assert.equal(frame.code, " var error = <Error> new CustomError('foo');");
assert.equal(frame.context.pre[0], ' }');
assert.equal(frame.context.pre[1], ' }');
assert.equal(frame.context.pre[2],
' // TypeScript code snippet will include `<Error>`');
assert.equal(frame.context.post[0], ' throw error;');
assert.equal(frame.context.post[1], '}');

var sourceMappingURLs = addItem.getCall(0).args[0].notifier.diagnostic
.node_source_maps.source_mapping_urls;
var urls = Object.keys(sourceMappingURLs);
assert.ok(urls.length === 1);
assert.ok(urls[0].includes('index.js'));
assert.ok(sourceMappingURLs[urls[0]].includes('index.js.map'));
}
addItem.reset();
}
}
}
})
.addBatch({
'handleItemWithError': {
'options': {
'anything': {
topic: function() {
Expand Down

0 comments on commit 1ee2cc6

Please sign in to comment.