Symbolicate JS stacktrace using RN Packager
Summary: The way we currently symbolicate JS stack traces in RN during development time (e.g. inside the RedBox) is the following: we download the source map from RN, parse it and use `source-map` find original file/line numbers. All happens inside running JSC VM in a simulator. The problem with this approach is that the source map size is pretty big and it is very expensive to load/parse. Before we load sourcemaps: {F60869250} After we load sourcemaps: {F60869249} In the past it wasn't a big problem, however the sourcemap file is only getting larger and soon we will be loading it for yellow boxes too: https://github.com/facebook/react-native/pull/7459 Moving stack trace symbolication to server side will let us: - save a bunch of memory on device - improve performance (no need to JSON serialize/deserialize and transfer sourcemap via HTTP and bridge) - remove ugly workaround with `RCTExceptionsManager.updateExceptionMessage` - we will be able to symbolicate from native by simply sending HTTP request, which means symbolication can be more robust (no need to depend on crashed JS to do symbolication) and we can pause JSC to avoid getting too many redboxes that hide original error. - reduce the bundle by ~65KB (the size of source-map parsing library we ship, see SourceMap module) Reviewed By: davidaurelio Differential Revision: D3291793 fbshipit-source-id: 29dce5f40100259264f57254e6715ace8ea70174
This commit is contained in:
parent
e4753867ea
commit
62e74f3832
|
@ -14,6 +14,7 @@ jest.setMock('worker-farm', function() { return () => {}; })
|
|||
.setMock('timers', { setImmediate: (fn) => setTimeout(fn, 0) })
|
||||
.setMock('uglify-js')
|
||||
.setMock('crypto')
|
||||
.setMock('source-map', { SourceMapConsumer: (fn) => {}})
|
||||
.mock('../../Bundler')
|
||||
.mock('../../AssetServer')
|
||||
.mock('../../lib/declareOpts')
|
||||
|
@ -21,6 +22,7 @@ jest.setMock('worker-farm', function() { return () => {}; })
|
|||
.mock('../../Activity');
|
||||
|
||||
const Promise = require('promise');
|
||||
const SourceMapConsumer = require('source-map').SourceMapConsumer;
|
||||
|
||||
const Bundler = require('../../Bundler');
|
||||
const Server = require('../');
|
||||
|
@ -392,4 +394,59 @@ describe('processRequest', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/symbolicate endpoint', () => {
|
||||
pit('should symbolicate given stack trace', () => {
|
||||
const body = JSON.stringify({stack: [{
|
||||
file: 'foo.bundle?platform=ios',
|
||||
lineNumber: 2100,
|
||||
column: 44,
|
||||
customPropShouldBeLeftUnchanged: 'foo',
|
||||
}]});
|
||||
|
||||
SourceMapConsumer.prototype.originalPositionFor = jest.fn((frame) => {
|
||||
expect(frame.line).toEqual(2100);
|
||||
expect(frame.column).toEqual(44);
|
||||
return {
|
||||
source: 'foo.js',
|
||||
line: 21,
|
||||
column: 4,
|
||||
};
|
||||
});
|
||||
|
||||
return makeRequest(
|
||||
requestHandler,
|
||||
'/symbolicate',
|
||||
{ rawBody: body }
|
||||
).then(response => {
|
||||
expect(JSON.parse(response.body)).toEqual({
|
||||
stack: [{
|
||||
file: 'foo.js',
|
||||
lineNumber: 21,
|
||||
column: 4,
|
||||
customPropShouldBeLeftUnchanged: 'foo',
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/symbolicate handles errors', () => {
|
||||
pit('should symbolicate given stack trace', () => {
|
||||
const body = 'clearly-not-json';
|
||||
console.error = jest.fn();
|
||||
|
||||
return makeRequest(
|
||||
requestHandler,
|
||||
'/symbolicate',
|
||||
{ rawBody: body }
|
||||
).then(response => {
|
||||
expect(response.statusCode).toEqual(500);
|
||||
expect(JSON.parse(response.body)).toEqual({
|
||||
error: jasmine.any(String),
|
||||
});
|
||||
expect(console.error).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ const FileWatcher = require('node-haste').FileWatcher;
|
|||
const getPlatformExtension = require('node-haste').getPlatformExtension;
|
||||
const Bundler = require('../Bundler');
|
||||
const Promise = require('promise');
|
||||
const SourceMapConsumer = require('source-map').SourceMapConsumer;
|
||||
|
||||
const _ = require('lodash');
|
||||
const declareOpts = require('../lib/declareOpts');
|
||||
|
@ -428,6 +429,9 @@ class Server {
|
|||
} else if (pathname.match(/^\/assets\//)) {
|
||||
this._processAssetsRequest(req, res);
|
||||
return;
|
||||
} else if (pathname === '/symbolicate') {
|
||||
this._symbolicate(req, res);
|
||||
return;
|
||||
} else {
|
||||
next();
|
||||
return;
|
||||
|
@ -480,6 +484,64 @@ class Server {
|
|||
).done();
|
||||
}
|
||||
|
||||
_symbolicate(req, res) {
|
||||
const startReqEventId = Activity.startEvent('symbolicate');
|
||||
new Promise.resolve(req.rawBody).then(body => {
|
||||
const stack = JSON.parse(body).stack;
|
||||
|
||||
// In case of multiple bundles / HMR, some stack frames can have
|
||||
// different URLs from others
|
||||
const urls = stack.map(frame => frame.file);
|
||||
const uniqueUrls = urls.filter((elem, idx) => urls.indexOf(elem) === idx);
|
||||
|
||||
const sourceMaps = uniqueUrls.map(sourceUrl => this._sourceMapForURL(sourceUrl));
|
||||
return Promise.all(sourceMaps).then(consumers => {
|
||||
return stack.map(frame => {
|
||||
const idx = uniqueUrls.indexOf(frame.file);
|
||||
const consumer = consumers[idx];
|
||||
|
||||
const original = consumer.originalPositionFor({
|
||||
line: frame.lineNumber,
|
||||
column: frame.column,
|
||||
});
|
||||
|
||||
if (!original) {
|
||||
return frame;
|
||||
}
|
||||
|
||||
return Object.assign({}, frame, {
|
||||
file: original.source,
|
||||
lineNumber: original.line,
|
||||
column: original.column,
|
||||
});
|
||||
});
|
||||
});
|
||||
}).then(
|
||||
stack => res.end(JSON.stringify({stack: stack})),
|
||||
error => {
|
||||
console.error(error.stack || error);
|
||||
res.statusCode = 500;
|
||||
res.end(JSON.stringify({error: error.message}));
|
||||
}
|
||||
).done(() => {
|
||||
Activity.endEvent(startReqEventId);
|
||||
});
|
||||
}
|
||||
|
||||
_sourceMapForURL(reqUrl) {
|
||||
const options = this._getOptionsFromUrl(reqUrl);
|
||||
const optionsJson = JSON.stringify(options);
|
||||
const building = this._bundles[optionsJson] || this.buildBundle(options);
|
||||
this._bundles[optionsJson] = building;
|
||||
return building.then(p => {
|
||||
const sourceMap = p.getSourceMap({
|
||||
minify: options.minify,
|
||||
dev: options.dev,
|
||||
});
|
||||
return new SourceMapConsumer(sourceMap);
|
||||
});
|
||||
}
|
||||
|
||||
_handleError(res, bundleID, error) {
|
||||
res.writeHead(error.status || 500, {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
|
|
Loading…
Reference in New Issue