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

Support capturing code context from sourcesContent in the source map file #959

Merged
merged 1 commit into from
Jun 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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