From 03d4168066e80788c4aebf3453d8ed7bd6cb65a8 Mon Sep 17 00:00:00 2001 From: Alex Tsokurov Date: Mon, 6 Jul 2020 18:44:10 +0200 Subject: [PATCH] getContext feature implementation --- README.md | 9 ++++ spec/stacktrace-gps-spec.js | 70 ++++++++++++++++++++++++++ stacktrace-gps.js | 98 +++++++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+) diff --git a/README.md b/README.md index 0f415d0..091b9af 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,11 @@ https://raw.githubusercontent.com/stacktracejs/stacktrace-gps/master/dist/stackt options: Object * **sourceCache: Object (String URL : String Source)** - Pre-populate source cache to avoid network requests * **sourceMapConsumerCache: Object (Source Mapping URL : SourceMap.SourceMapConsumer)** - Pre-populate source cache to avoid network requests +* **sourceCache: Object (String URL : String Source)** - Pre-populate source cache to avoid network requests +* **sourceMapConsumerCache: Object (Source Mapping URL : SourceMap.SourceMapConsumer)** - Pre-populate source cache to avoid network requests +* **offline: Boolean (default false)** - Set to `true` to prevent all network requests +* **contextMaxLineLength: Number (default 200)** - The maximum length of one line in the context source code +* **contextMaxLinesCount: Number (default 5)** - The maximum number of lines in the context source code * **offline: Boolean (default false)** - Set to `true` to prevent all network requests * **ajax: Function (String URL => Promise(responseText))** - Function to be used for making X-Domain requests * **atob: Function (String => String)** - Function to convert base64-encoded strings to their original representation @@ -79,6 +84,10 @@ Enhance function name and use source maps to produce a better StackFrame. Enhance function name and use source maps to produce a better StackFrame. * **stackframe** - [StackFrame](https://github.com/stacktracejs/stackframe) or like object +#### `.getContext(stackframe)` => Promise({ source, lineNumber, columnNumber }) +Returns the surrounding source code chunk (with the start position) for where the stackframe points. +* **stackframe** - [StackFrame](https://github.com/stacktracejs/stackframe) or like object + ## Browser Support [![Sauce Test Status](https://saucelabs.com/browser-matrix/stacktracejs.svg)](https://saucelabs.com/u/stacktracejs) diff --git a/spec/stacktrace-gps-spec.js b/spec/stacktrace-gps-spec.js index 711161a..5846f6b 100644 --- a/spec/stacktrace-gps-spec.js +++ b/spec/stacktrace-gps-spec.js @@ -442,4 +442,74 @@ describe('StackTraceGPS', function() { } }); }); + + describe('#_getContextSingleLineIndexes', function() { + it('returns indexes that determine the correct range', function(done) { + test(1); + test(4); + test(5); + done(); + + function test(contextMaxLineLength) { + var stackTraceGPS = new StackTraceGPS({ contextMaxLineLength: contextMaxLineLength }); + var line = ''; + for (var length = 1; length <= 10; length++) { + line += 'x'; + for (var columnNumber = 1; columnNumber <= 1; columnNumber++) { + var indexes = stackTraceGPS._getContextSingleLineIndexes(line, columnNumber); + expect(indexes[1] - indexes[0]).toBe(Math.min(contextMaxLineLength, length)); + expect(indexes[1] >= columnNumber && columnNumber > indexes[0]).toBe(true); + } + } + } + }); + }); + + describe('#_getContextMultipleLinesIndexes', function() { + it('returns indexes that determine the correct range', function(done) { + test(1); + test(4); + test(5); + done(); + + function test(contextMaxLinesCount) { + var stackTraceGPS = new StackTraceGPS({ contextMaxLinesCount: contextMaxLinesCount }); + var lines = []; + for (var count = 1; count <= 10; count++) { + lines.push('x'); + for (var columnNumber = 1; columnNumber <= 1; columnNumber++) { + var indexes = stackTraceGPS._getContextMultipleLinesIndexes(lines, columnNumber); + expect(indexes[1] - indexes[0]).toBe(Math.min(contextMaxLinesCount, count)); + expect(indexes[1] >= columnNumber && columnNumber > indexes[0]).toBe(true); + } + } + } + }); + + it('avoids too long lines', function(done) { + var lines = [ + 'line', + 'too long line', + 'line', + 'line', + 'line', + 'too long line', + 'line' + ]; + var stackTraceGPS = new StackTraceGPS({ contextMaxLineLength: 4, contextMaxLinesCount: 2 }); + expect(stackTraceGPS._getContextMultipleLinesIndexes(lines, 1)).toEqual([0, 1]); + expect(stackTraceGPS._getContextMultipleLinesIndexes(lines, 3)).toEqual([2, 4]); + expect(stackTraceGPS._getContextMultipleLinesIndexes(lines, 4)).toEqual([2, 4]); + expect(stackTraceGPS._getContextMultipleLinesIndexes(lines, 5)).toEqual([3, 5]); + expect(stackTraceGPS._getContextMultipleLinesIndexes(lines, 7)).toEqual([6, 7]); + stackTraceGPS.contextMaxLinesCount = 5; + expect(stackTraceGPS._getContextMultipleLinesIndexes(lines, 1)).toEqual([0, 1]); + expect(stackTraceGPS._getContextMultipleLinesIndexes(lines, 3)).toEqual([2, 5]); + expect(stackTraceGPS._getContextMultipleLinesIndexes(lines, 4)).toEqual([2, 5]); + expect(stackTraceGPS._getContextMultipleLinesIndexes(lines, 5)).toEqual([2, 5]); + expect(stackTraceGPS._getContextMultipleLinesIndexes(lines, 7)).toEqual([6, 7]); + done(); + }); + }); + }); diff --git a/stacktrace-gps.js b/stacktrace-gps.js index 294c8c8..f4cf546 100644 --- a/stacktrace-gps.js +++ b/stacktrace-gps.js @@ -174,6 +174,8 @@ * @param {Object} opts * opts.sourceCache = {url: "Source String"} => preload source cache * opts.sourceMapConsumerCache = {/path/file.js.map: SourceMapConsumer} + * opts.contextMaxLineLength = {Number} + * opts.contextMaxLinesCount = {Number} * opts.offline = True to prevent network requests. * Best effort without sources or source maps. * opts.ajax = Promise returning function to make X-Domain requests @@ -187,6 +189,9 @@ this.sourceCache = opts.sourceCache || {}; this.sourceMapConsumerCache = opts.sourceMapConsumerCache || {}; + this.contextMaxLineLength = opts.contextMaxLineLength || 200; + this.contextMaxLinesCount = opts.contextMaxLinesCount || 5; + this.ajax = opts.ajax || _xdr; this._atob = opts.atob || _atob; @@ -338,5 +343,98 @@ }.bind(this), reject)['catch'](reject); }.bind(this)); }; + + /** + * Given the source code line and the column number, + * truncate the line to contextMaxLineLength, + * return the start and end indexes of the truncated line. + * + * @param {String} line + * @param {Number} columnNumber + * @returns {Object} + */ + this._getContextSingleLineIndexes = function _getContextSingleLineIndexes(line, columnNumber) { + var indexStart = columnNumber - 1 - Math.floor(this.contextMaxLineLength / 2); + if (indexStart < 0) { + indexStart = 0; + } + var indexEnd = indexStart + this.contextMaxLineLength; + if (indexEnd > line.length) { + indexEnd = line.length; + indexStart = indexEnd - this.contextMaxLineLength; + if (indexStart < 0) { + indexStart = 0; + } + } + return [indexStart, indexEnd]; + }; + + /** + * Given the source code lines and the line number, + * truncate the lines array to contextMaxLinesCount, + * avoid the lines with length more than contextMaxLineLength, + * return the object with the lines subarray and its start line number. + * + * @param {Array.String} lines + * @param {Number} lineNumber + * @returns {Object} + */ + this._getContextMultipleLinesIndexes = function _getContextMultipleLinesIndexes(lines, lineNumber) { + var indexes = [lineNumber - 1, lineNumber]; + var active = [true, true]; + var step = 0; + while (indexes[1] - indexes[0] < this.contextMaxLinesCount) { + var line = lines[indexes[step] + step - 1]; + if (line === undefined || line.length > this.contextMaxLineLength) { + active[step] = false; + } else { + indexes[step] += 2 * step - 1; + } + if (active[1 - step]) { + step = 1 - step; + } else if (!active[step]) { + break; + } + } + return indexes; + }; + + /** + * Given a StackFrame, return a surrounding source code part with its start position. + * + * @param {StackFrame} stackframe + * @returns {Promise} + */ + this.getContext = function StackTraceGPS$$getContext(stackframe) { + return new Promise(function(resolve, reject) { + _ensureSupportedEnvironment(); + _ensureStackFrameIsLegit(stackframe); + + var fileName = stackframe.fileName; + var lineNumber = stackframe.lineNumber; + var columnNumber = stackframe.columnNumber; + this._get(fileName).then(function(source) { + var lines = source.split('\n'); + var line = lines[lineNumber - 1]; + var indexes, context; + if (line.length > this.contextMaxLineLength) { + indexes = this._getContextSingleLineIndexes(line, columnNumber); + context = { + source: line.substring(indexes[0], indexes[1]), + lineNumber: lineNumber, + columnNumber: indexes[0] + 1 + }; + } else { + indexes = this._getContextMultipleLinesIndexes(lines, lineNumber); + context = { + source: lines.slice(indexes[0], indexes[1]).join('\n'), + lineNumber: indexes[0] + 1, + columnNumber: 0 + }; + } + resolve(context); + }.bind(this), reject)['catch'](reject); + }.bind(this)); + }; }; }));