提交 5831bdcb 编写于 作者: D DCloud_LXH

fix(stacktracey): handle weixin sourcemap

上级 bad64b31
......@@ -34,26 +34,23 @@ describe('uni-stacktracey', () => {
test('uniStracktraceyPreset local', () => {
stacktracey(uniErrorMsg, {
preset: uniStracktraceyPreset({
base: path.resolve(
__dirname,
'../test/__UNI__APPID__/1.0.0/.sourcemap/h5/'
),
base: path.resolve(__dirname, '../test/__UNI_APPID__/h5/1.0.0/'),
sourceRoot: '',
}),
}).then((res: string) => {
expect(res).toEqual(`Error: Sentry Error
at src/pages/index/index.vue:44
at src/pages/index/index.vue?be58:12
at node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue/dist/vue.runtime.esm.js:1864
at node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue/dist/vue.runtime.esm.js:2189
at node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue/dist/vue.runtime.esm.js:1864
at node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue/dist/vue.runtime.esm.js:2185
at node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue/dist/vue.runtime.esm.js:7076
at node_modules/@sentry/browser/esm/helpers.js:74 `)
at src/pages/index/index.vue:44
at src/pages/index/index.vue?be58:12:17
at node_modules/@sentry/browser/esm/helpers.js:74:22
at node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue/dist/vue.runtime.esm.js:1864:25
at node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue/dist/vue.runtime.esm.js:2189:13
at node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue/dist/vue.runtime.esm.js:1864:25
at node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue/dist/vue.runtime.esm.js:2185:8
at node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue/dist/vue.runtime.esm.js:7076:24`)
})
})
test('uniStracktraceyPreset local', () => {
test('utsStracktraceyPreset local', () => {
stacktracey(utsErrorMsg, {
preset: utsStracktraceyPreset({
base: path.resolve(
......
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var fs = require('fs');
var StackTracey = require('stacktracey');
var sourceMap = require('source-map');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
var StackTracey__default = /*#__PURE__*/_interopDefaultLegacy(StackTracey);
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var fs = require('fs');
var sourceMap = require('source-map');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
/* ------------------------------------------------------------------------ */
const O = Object, isBrowser = typeof window !== 'undefined' &&
window.window === window &&
window.navigator, nodeRequire = isBrowser ? null : module.require, // to prevent bundlers from expanding the require call
lastOf = (x) => x[x.length - 1], nixSlashes$1 = (x) => x.replace(/\\/g, '/'), pathRoot = isBrowser ? window.location.href : nixSlashes$1(process.cwd()) + '/';
/* ------------------------------------------------------------------------ */
class StackTracey {
constructor(input, offset) {
this.itemsHeader = [];
this.isMP = false;
const originalInput = input, isParseableSyntaxError = input && input instanceof SyntaxError && !isBrowser;
/* new StackTracey () */
if (!input) {
input = new Error();
offset = offset === undefined ? 1 : offset;
}
/* new StackTracey (Error) */
if (input instanceof Error) {
input = input.stack || '';
}
/* new StackTracey (string) */
if (typeof input === 'string') {
this.isMP = input.indexOf('MiniProgramError') !== -1;
input = this.rawParse(input)
.slice(offset)
.map((x) => this.extractEntryMetadata(x));
}
/* new StackTracey (array) */
if (Array.isArray(input)) {
if (isParseableSyntaxError) {
const rawLines = nodeRequire('util')
.inspect(originalInput)
.split('\n'), fileLine = rawLines[0].split(':'), line = fileLine.pop(), file = fileLine.join(':');
if (file) {
input.unshift({
file: nixSlashes$1(file),
line: line,
column: (rawLines[2] || '').indexOf('^') + 1,
sourceLine: rawLines[1],
callee: '(syntax error)',
syntaxError: true,
});
}
}
this.items = input;
}
else {
this.items = [];
}
}
extractEntryMetadata(e) {
const decomposedPath = this.decomposePath(e.file || '');
const fileRelative = decomposedPath[0];
const externalDomain = decomposedPath[1];
return O.assign(e, {
calleeShort: e.calleeShort || lastOf((e.callee || '').split('.')),
fileRelative: fileRelative,
fileShort: this.shortenPath(fileRelative),
fileName: lastOf((e.file || '').split('/')),
thirdParty: this.isThirdParty(fileRelative, externalDomain) && !e.index,
externalDomain: externalDomain,
});
}
shortenPath(relativePath) {
return relativePath
.replace(/^node_modules\//, '')
.replace(/^webpack\/bootstrap\//, '')
.replace(/^__parcel_source_root\//, '');
}
decomposePath(fullPath) {
let result = fullPath;
if (isBrowser)
result = result.replace(pathRoot, '');
const externalDomainMatch = result.match(/^(http|https)\:\/\/?([^\/]+)\/(.*)/);
const externalDomain = externalDomainMatch
? externalDomainMatch[2]
: undefined;
result = externalDomainMatch ? externalDomainMatch[3] : result;
// if (!isBrowser) result = nodeRequire!('path').relative(pathRoot, result)
return [
nixSlashes$1(result).replace(/^.*\:\/\/?\/?/, ''),
externalDomain,
];
}
isThirdParty(relativePath, externalDomain) {
if (this.isMP) {
if (typeof externalDomain === 'undefined')
return false;
return externalDomain !== 'usr';
}
return (externalDomain ||
relativePath[0] === '~' || // webpack-specific heuristic
relativePath[0] === '/' || // external source
relativePath.indexOf('@dcloudio') !== -1 ||
relativePath.indexOf('webpack/bootstrap') === 0);
}
rawParse(str) {
const lines = (str || '').split('\n');
const entries = lines.map((line, index) => {
line = line.trim();
let callee, fileLineColumn = [], native, planA, planB;
if ((planA = line.match(/at (.+) \(eval at .+ \((.+)\), .+\)/)) || // eval calls
(planA = line.match(/at (.+) \((.+)\)/)) ||
(line.slice(0, 3) !== 'at ' && (planA = line.match(/(.*)@(.*)/)))) {
this.itemsHeader.push('%StacktraceyItem%');
callee = planA[1];
native = planA[2] === 'native';
fileLineColumn = (planA[2].match(/(.*):(\d+):(\d+)/) ||
planA[2].match(/(.*):(\d+)/) ||
planA[2].match(/\[(.*)\]/) ||
[]).slice(1);
}
else if ((planB = line.match(/^(at\s*)*(.*)\s+(.+):(\d+):(\d+)/))) {
this.itemsHeader.push('%StacktraceyItem%');
callee = planB[2].trim();
fileLineColumn = planB.slice(3);
}
else {
this.itemsHeader.push(line);
return undefined;
}
/* Detect things like Array.reduce
TODO: detect more built-in types */
if (callee && !fileLineColumn[0]) {
const type = callee.split('.')[0];
if (type === 'Array') {
native = true;
}
}
return {
beforeParse: line,
callee: callee || '',
index: isBrowser && fileLineColumn[0] === window.location.href,
native: native || false,
file: nixSlashes$1(fileLineColumn[0] || ''),
line: parseInt(fileLineColumn[1] || '', 10) || undefined,
column: parseInt(fileLineColumn[2] || '', 10) || undefined,
};
});
return entries.filter((x) => x !== undefined);
}
maxColumnWidths() {
return {
callee: 30,
file: 60,
sourceLine: 80,
};
}
asTable(opts) {
const maxColumnWidths = (opts && opts.maxColumnWidths) || this.maxColumnWidths();
const trimmed = this
.filter((e) => !e.thirdParty)
.map((e) => parseItem(e, maxColumnWidths, this.isMP));
const trimmedThirdParty = this
.filter((e) => e.thirdParty)
.map((e) => parseItem(e, maxColumnWidths, this.isMP));
return {
items: trimmed.items,
thirdPartyItems: trimmedThirdParty.items,
};
}
}
const trimEnd = (s, n) => s && (s.length > n ? s.slice(0, n - 1) + '' : s);
const trimStart = (s, n) => s && (s.length > n ? '' + s.slice(-(n - 1)) : s);
function parseItem(e, maxColumnWidths, isMP) {
const filePath = (isMP ? e.file && e.file : e.fileShort && e.fileShort) +
`${e.line ? ':' + e.line : ''}` +
`${e.column ? ':' + e.column : ''}`;
return [
'at ' + trimEnd(isMP ? e.callee : e.calleeShort, maxColumnWidths.callee),
trimStart(filePath || '', maxColumnWidths.file),
trimEnd((e.sourceLine || '').trim() || '', maxColumnWidths.sourceLine),
];
}
['map', 'filter', 'slice', 'concat'].forEach((method) => {
StackTracey.prototype[method] = function ( /*...args */) {
// no support for ...args in Node v4 :(
return new StackTracey(this.items[method].apply(this.items, arguments));
};
});
/* ------------------------------------------------------------------------ */
var StackTracey$1 = StackTracey;
const nixSlashes = (x) => x.replace(/\\/g, '/');
const sourcemapCatch = {};
function stacktracey(stacktrace, opts) {
const parseStack = [];
const stack = opts.preset.parseStacktrace(stacktrace);
stack.items.forEach((item, index) => {
const fn = () => {
const fn = (item, index) => {
const { line = 0, column = 0, file, fileName, fileRelative } = item;
try {
if (item.thirdParty) {
return Promise.resolve();
}
function _getSourceMapContent(file, fileName, fileRelative) {
return opts.preset
.getSourceMapContent(file, fileName, fileRelative)
.then((content) => {
if (content) {
return getConsumer(content).then((consumer) => {
const sourceMapContent = parseSourceMapContent(consumer, {
return parseSourceMapContent(consumer, {
line,
column,
});
if (sourceMapContent) {
const { source, sourcePath, sourceLine, sourceColumn, fileName = '', } = sourceMapContent;
stack.items[index] = Object.assign({}, item, {
file: source,
line: sourceLine,
column: sourceColumn,
fileShort: sourcePath,
fileRelative: sourcePath,
fileName,
});
}
});
}
});
}
try {
return _getSourceMapContent(file, fileName, fileRelative).then((sourceMapContent) => {
if (sourceMapContent) {
const { source, sourcePath, sourceLine, sourceColumn, fileName = '', } = sourceMapContent;
stack.items[index] = Object.assign({}, item, {
file: source,
line: sourceLine,
column: sourceColumn,
fileShort: sourcePath,
fileRelative: source,
fileName,
thirdParty: isThirdParty(sourcePath),
});
/**
* 以 .js 结尾
* 包含 app-service.js 则需要再解析 两次
* 不包含 app-service.js 则无需再解析 一次
*/
const curItem = stack.items[index];
if (stack.isMP &&
curItem.beforeParse.indexOf('app-service') !== -1) {
return fn(curItem, index);
}
}
});
}
catch (error) {
return Promise.resolve();
}
};
parseStack.push(fn());
parseStack.push(fn(item, index));
});
return new Promise((resolve, reject) => {
Promise.all(parseStack)
......@@ -69,6 +268,9 @@ function stacktracey(stacktrace, opts) {
});
});
}
function isThirdParty(relativePath) {
return relativePath.indexOf('@dcloudio') !== -1;
}
function getConsumer(content) {
return new Promise((resolve, reject) => {
if (sourceMap.SourceMapConsumer.with) {
......@@ -126,9 +328,24 @@ function parseSourceMapContent(consumer, obj) {
};
}
}
function joinItem(item) {
const a = item[0];
const b = item[1] ? ` ${item[1]}` : '';
const c = item[2] ? ` ${item[2]}` : '';
return `${a}${b}${c}`;
}
function uniStracktraceyPreset(opts) {
const { base, sourceRoot } = opts;
const { base, sourceRoot, splitThirdParty } = opts;
let stack;
return {
/**
*
* 微信特殊处理
* 微信解析步骤:
* 1. //usr/app-service.js -> 'weixin/__APP__/app-service.map.map'
* 2. //usr/pages/API/app-service.js -> 'weixin/pages/API/app-service.map.map'
* 3. uni-list-item/uni-list-item.js -> ${base}/uni-list-item/uni-list-item.js.map
*/
parseSourceMapUrl(file, fileName, fileRelative) {
// 组合 sourceMapUrl
if (fileRelative.indexOf('(') !== -1)
......@@ -138,18 +355,62 @@ function uniStracktraceyPreset(opts) {
if (sourceRoot) {
return `${fileRelative.replace(sourceRoot, base + '/')}.map`;
}
return `${base}/${fileRelative}.map`;
let baseAfter = '';
if (stack.isMP) {
if (fileRelative.indexOf('app-service.js') !== -1) {
baseAfter = (base.match(/\w$/) ? '/' : '') + 'weixin';
if (fileRelative === fileName) {
baseAfter += '/__APP__';
}
fileRelative = fileRelative.replace('.js', '.map');
}
if (baseAfter && !!fileRelative.match(/^\w/))
baseAfter += '/';
}
return `${base}${baseAfter}${fileRelative}.map`;
},
getSourceMapContent(file, fileName, fileRelative) {
return Promise.resolve(getSourceMapContent(this.parseSourceMapUrl(file, fileName, fileRelative)));
if (stack.isMP && fileRelative.indexOf('.js') === -1) {
return Promise.resolve('');
}
const sourcemapUrl = this.parseSourceMapUrl(file, fileName, fileRelative);
return Promise.resolve(getSourceMapContent(sourcemapUrl));
},
parseStacktrace(stacktrace) {
return new StackTracey__default["default"](stacktrace);
stack = new StackTracey$1(stacktrace);
return stack;
},
asTableStacktrace({ maxColumnWidths, stacktrace, stack }) {
const errorName = stacktrace.split('\n')[0];
return ((errorName.indexOf('at') === -1 ? `${errorName}\n` : '') +
(stack.asTable ? stack.asTable({ maxColumnWidths }) : ''));
const lines = stack.asTable
? stack.asTable(maxColumnWidths ? { maxColumnWidths } : undefined)
: { items: [], thirdPartyItems: [] };
if (lines.items.length || lines.thirdPartyItems.length) {
const { items: stackLines, thirdPartyItems: stackThirdPartyLines } = lines;
const userError = stack.itemsHeader
.map((item) => {
if (item === '%StacktraceyItem%') {
const _stack = stackLines.shift();
return _stack ? joinItem(_stack) : '';
}
return item;
})
.filter(Boolean)
.join('\n');
const thirdParty = stackThirdPartyLines.length
? stackThirdPartyLines.map(joinItem).join('\n')
: '';
if (splitThirdParty) {
return {
userError,
thirdParty,
};
}
return userError + '\n' + thirdParty;
}
else {
return errorName;
}
},
};
}
......@@ -205,6 +466,7 @@ function utsStracktraceyPreset(opts) {
.filter((x) => x !== undefined);
return {
items: entries,
itemsHeader: [],
};
},
asTableStacktrace({ maxColumnWidths, stacktrace, stack }) {
......@@ -220,8 +482,8 @@ function utsStracktraceyPreset(opts) {
.join('\n');
},
};
}
exports.stacktracey = stacktracey;
exports.uniStracktraceyPreset = uniStracktraceyPreset;
exports.utsStracktraceyPreset = utsStracktraceyPreset;
}
exports.stacktracey = stacktracey;
exports.uniStracktraceyPreset = uniStracktraceyPreset;
exports.utsStracktraceyPreset = utsStracktraceyPreset;
import fs from 'fs';
import StackTracey from 'stacktracey';
import { SourceMapConsumer } from 'source-map';
/* ------------------------------------------------------------------------ */
const O = Object, isBrowser = typeof window !== 'undefined' &&
window.window === window &&
window.navigator, nodeRequire = isBrowser ? null : module.require, // to prevent bundlers from expanding the require call
lastOf = (x) => x[x.length - 1], nixSlashes$1 = (x) => x.replace(/\\/g, '/'), pathRoot = isBrowser ? window.location.href : nixSlashes$1(process.cwd()) + '/';
/* ------------------------------------------------------------------------ */
class StackTracey {
constructor(input, offset) {
this.itemsHeader = [];
this.isMP = false;
const originalInput = input, isParseableSyntaxError = input && input instanceof SyntaxError && !isBrowser;
/* new StackTracey () */
if (!input) {
input = new Error();
offset = offset === undefined ? 1 : offset;
}
/* new StackTracey (Error) */
if (input instanceof Error) {
input = input.stack || '';
}
/* new StackTracey (string) */
if (typeof input === 'string') {
this.isMP = input.indexOf('MiniProgramError') !== -1;
input = this.rawParse(input)
.slice(offset)
.map((x) => this.extractEntryMetadata(x));
}
/* new StackTracey (array) */
if (Array.isArray(input)) {
if (isParseableSyntaxError) {
const rawLines = nodeRequire('util')
.inspect(originalInput)
.split('\n'), fileLine = rawLines[0].split(':'), line = fileLine.pop(), file = fileLine.join(':');
if (file) {
input.unshift({
file: nixSlashes$1(file),
line: line,
column: (rawLines[2] || '').indexOf('^') + 1,
sourceLine: rawLines[1],
callee: '(syntax error)',
syntaxError: true,
});
}
}
this.items = input;
}
else {
this.items = [];
}
}
extractEntryMetadata(e) {
const decomposedPath = this.decomposePath(e.file || '');
const fileRelative = decomposedPath[0];
const externalDomain = decomposedPath[1];
return O.assign(e, {
calleeShort: e.calleeShort || lastOf((e.callee || '').split('.')),
fileRelative: fileRelative,
fileShort: this.shortenPath(fileRelative),
fileName: lastOf((e.file || '').split('/')),
thirdParty: this.isThirdParty(fileRelative, externalDomain) && !e.index,
externalDomain: externalDomain,
});
}
shortenPath(relativePath) {
return relativePath
.replace(/^node_modules\//, '')
.replace(/^webpack\/bootstrap\//, '')
.replace(/^__parcel_source_root\//, '');
}
decomposePath(fullPath) {
let result = fullPath;
if (isBrowser)
result = result.replace(pathRoot, '');
const externalDomainMatch = result.match(/^(http|https)\:\/\/?([^\/]+)\/(.*)/);
const externalDomain = externalDomainMatch
? externalDomainMatch[2]
: undefined;
result = externalDomainMatch ? externalDomainMatch[3] : result;
// if (!isBrowser) result = nodeRequire!('path').relative(pathRoot, result)
return [
nixSlashes$1(result).replace(/^.*\:\/\/?\/?/, ''),
externalDomain,
];
}
isThirdParty(relativePath, externalDomain) {
if (this.isMP) {
if (typeof externalDomain === 'undefined')
return false;
return externalDomain !== 'usr';
}
return (externalDomain ||
relativePath[0] === '~' || // webpack-specific heuristic
relativePath[0] === '/' || // external source
relativePath.indexOf('@dcloudio') !== -1 ||
relativePath.indexOf('webpack/bootstrap') === 0);
}
rawParse(str) {
const lines = (str || '').split('\n');
const entries = lines.map((line, index) => {
line = line.trim();
let callee, fileLineColumn = [], native, planA, planB;
if ((planA = line.match(/at (.+) \(eval at .+ \((.+)\), .+\)/)) || // eval calls
(planA = line.match(/at (.+) \((.+)\)/)) ||
(line.slice(0, 3) !== 'at ' && (planA = line.match(/(.*)@(.*)/)))) {
this.itemsHeader.push('%StacktraceyItem%');
callee = planA[1];
native = planA[2] === 'native';
fileLineColumn = (planA[2].match(/(.*):(\d+):(\d+)/) ||
planA[2].match(/(.*):(\d+)/) ||
planA[2].match(/\[(.*)\]/) ||
[]).slice(1);
}
else if ((planB = line.match(/^(at\s*)*(.*)\s+(.+):(\d+):(\d+)/))) {
this.itemsHeader.push('%StacktraceyItem%');
callee = planB[2].trim();
fileLineColumn = planB.slice(3);
}
else {
this.itemsHeader.push(line);
return undefined;
}
/* Detect things like Array.reduce
TODO: detect more built-in types */
if (callee && !fileLineColumn[0]) {
const type = callee.split('.')[0];
if (type === 'Array') {
native = true;
}
}
return {
beforeParse: line,
callee: callee || '',
index: isBrowser && fileLineColumn[0] === window.location.href,
native: native || false,
file: nixSlashes$1(fileLineColumn[0] || ''),
line: parseInt(fileLineColumn[1] || '', 10) || undefined,
column: parseInt(fileLineColumn[2] || '', 10) || undefined,
};
});
return entries.filter((x) => x !== undefined);
}
maxColumnWidths() {
return {
callee: 30,
file: 60,
sourceLine: 80,
};
}
asTable(opts) {
const maxColumnWidths = (opts && opts.maxColumnWidths) || this.maxColumnWidths();
const trimmed = this
.filter((e) => !e.thirdParty)
.map((e) => parseItem(e, maxColumnWidths, this.isMP));
const trimmedThirdParty = this
.filter((e) => e.thirdParty)
.map((e) => parseItem(e, maxColumnWidths, this.isMP));
return {
items: trimmed.items,
thirdPartyItems: trimmedThirdParty.items,
};
}
}
const trimEnd = (s, n) => s && (s.length > n ? s.slice(0, n - 1) + '' : s);
const trimStart = (s, n) => s && (s.length > n ? '' + s.slice(-(n - 1)) : s);
function parseItem(e, maxColumnWidths, isMP) {
const filePath = (isMP ? e.file && e.file : e.fileShort && e.fileShort) +
`${e.line ? ':' + e.line : ''}` +
`${e.column ? ':' + e.column : ''}`;
return [
'at ' + trimEnd(isMP ? e.callee : e.calleeShort, maxColumnWidths.callee),
trimStart(filePath || '', maxColumnWidths.file),
trimEnd((e.sourceLine || '').trim() || '', maxColumnWidths.sourceLine),
];
}
['map', 'filter', 'slice', 'concat'].forEach((method) => {
StackTracey.prototype[method] = function ( /*...args */) {
// no support for ...args in Node v4 :(
return new StackTracey(this.items[method].apply(this.items, arguments));
};
});
/* ------------------------------------------------------------------------ */
var StackTracey$1 = StackTracey;
// @ts-ignore
{
// @ts-ignore
......@@ -18,38 +200,56 @@ function stacktracey(stacktrace, opts) {
const parseStack = [];
const stack = opts.preset.parseStacktrace(stacktrace);
stack.items.forEach((item, index) => {
const fn = () => {
const fn = (item, index) => {
const { line = 0, column = 0, file, fileName, fileRelative } = item;
try {
if (item.thirdParty) {
return Promise.resolve();
}
function _getSourceMapContent(file, fileName, fileRelative) {
return opts.preset
.getSourceMapContent(file, fileName, fileRelative)
.then((content) => {
if (content) {
return getConsumer(content).then((consumer) => {
const sourceMapContent = parseSourceMapContent(consumer, {
return parseSourceMapContent(consumer, {
line,
column,
});
if (sourceMapContent) {
const { source, sourcePath, sourceLine, sourceColumn, fileName = '', } = sourceMapContent;
stack.items[index] = Object.assign({}, item, {
file: source,
line: sourceLine,
column: sourceColumn,
fileShort: sourcePath,
fileRelative: sourcePath,
fileName,
});
}
});
}
});
}
try {
return _getSourceMapContent(file, fileName, fileRelative).then((sourceMapContent) => {
if (sourceMapContent) {
const { source, sourcePath, sourceLine, sourceColumn, fileName = '', } = sourceMapContent;
stack.items[index] = Object.assign({}, item, {
file: source,
line: sourceLine,
column: sourceColumn,
fileShort: sourcePath,
fileRelative: source,
fileName,
thirdParty: isThirdParty(sourcePath),
});
/**
* 以 .js 结尾
* 包含 app-service.js 则需要再解析 两次
* 不包含 app-service.js 则无需再解析 一次
*/
const curItem = stack.items[index];
if (stack.isMP &&
curItem.beforeParse.indexOf('app-service') !== -1) {
return fn(curItem, index);
}
}
});
}
catch (error) {
return Promise.resolve();
}
};
parseStack.push(fn());
parseStack.push(fn(item, index));
});
return new Promise((resolve, reject) => {
Promise.all(parseStack)
......@@ -70,6 +270,9 @@ function stacktracey(stacktrace, opts) {
});
});
}
function isThirdParty(relativePath) {
return relativePath.indexOf('@dcloudio') !== -1;
}
function getConsumer(content) {
return new Promise((resolve, reject) => {
if (SourceMapConsumer.with) {
......@@ -127,9 +330,24 @@ function parseSourceMapContent(consumer, obj) {
};
}
}
function joinItem(item) {
const a = item[0];
const b = item[1] ? ` ${item[1]}` : '';
const c = item[2] ? ` ${item[2]}` : '';
return `${a}${b}${c}`;
}
function uniStracktraceyPreset(opts) {
const { base, sourceRoot } = opts;
const { base, sourceRoot, splitThirdParty } = opts;
let stack;
return {
/**
*
* 微信特殊处理
* 微信解析步骤:
* 1. //usr/app-service.js -> 'weixin/__APP__/app-service.map.map'
* 2. //usr/pages/API/app-service.js -> 'weixin/pages/API/app-service.map.map'
* 3. uni-list-item/uni-list-item.js -> ${base}/uni-list-item/uni-list-item.js.map
*/
parseSourceMapUrl(file, fileName, fileRelative) {
// 组合 sourceMapUrl
if (fileRelative.indexOf('(') !== -1)
......@@ -139,18 +357,62 @@ function uniStracktraceyPreset(opts) {
if (sourceRoot) {
return `${fileRelative.replace(sourceRoot, base + '/')}.map`;
}
return `${base}/${fileRelative}.map`;
let baseAfter = '';
if (stack.isMP) {
if (fileRelative.indexOf('app-service.js') !== -1) {
baseAfter = (base.match(/\w$/) ? '/' : '') + 'weixin';
if (fileRelative === fileName) {
baseAfter += '__APP__';
}
fileRelative = fileRelative.replace('.js', '.map');
}
if (baseAfter && !!fileRelative.match(/^\w/))
baseAfter += '/';
}
return `${base}${baseAfter}${fileRelative}.map`;
},
getSourceMapContent(file, fileName, fileRelative) {
return Promise.resolve(getSourceMapContent(this.parseSourceMapUrl(file, fileName, fileRelative)));
if (stack.isMP && fileRelative.indexOf('.js') === -1) {
return Promise.resolve('');
}
const sourcemapUrl = this.parseSourceMapUrl(file, fileName, fileRelative);
return Promise.resolve(getSourceMapContent(sourcemapUrl));
},
parseStacktrace(stacktrace) {
return new StackTracey(stacktrace);
stack = new StackTracey$1(stacktrace);
return stack;
},
asTableStacktrace({ maxColumnWidths, stacktrace, stack }) {
const errorName = stacktrace.split('\n')[0];
return ((errorName.indexOf('at') === -1 ? `${errorName}\n` : '') +
(stack.asTable ? stack.asTable({ maxColumnWidths }) : ''));
const lines = stack.asTable
? stack.asTable(maxColumnWidths ? { maxColumnWidths } : undefined)
: { items: [], thirdPartyItems: [] };
if (lines.items.length || lines.thirdPartyItems.length) {
const { items: stackLines, thirdPartyItems: stackThirdPartyLines } = lines;
const userError = stack.itemsHeader
.map((item) => {
if (item === '%StacktraceyItem%') {
const _stack = stackLines.shift();
return _stack ? joinItem(_stack) : '';
}
return item;
})
.filter(Boolean)
.join('\n');
const thirdParty = stackThirdPartyLines.length
? stackThirdPartyLines.map(joinItem).join('\n')
: '';
if (splitThirdParty) {
return {
userError,
thirdParty,
};
}
return userError + '\n' + thirdParty;
}
else {
return errorName;
}
},
};
}
......@@ -206,6 +468,7 @@ function utsStracktraceyPreset(opts) {
.filter((x) => x !== undefined);
return {
items: entries,
itemsHeader: [],
};
},
asTableStacktrace({ maxColumnWidths, stacktrace, stack }) {
......
import fs from 'fs'
import StackTracey from 'stacktracey'
import StackTracey from './stacktracey'
import {
SourceMapConsumer,
BasicSourceMapConsumer,
......@@ -27,8 +27,17 @@ type StacktraceyItems = StackTracey.Entry & {
}
type Stacktracey = {
items: StacktraceyItems[]
itemsHeader: StackTracey['itemsHeader']
asTable?: StackTracey['asTable']
}
type asTableResult =
| string
| {
userError: string
thirdParty: string
}
interface StacktraceyPreset {
/**
* 解析错误栈信息
......@@ -43,7 +52,7 @@ interface StacktraceyPreset {
stack: Stacktracey
maxColumnWidths?: StackTracey.MaxColumnWidths
stacktrace: string
}): string
}): asTableResult
/**
* 编译后的文件名地址
* @param file
......@@ -69,51 +78,82 @@ interface StacktraceyOptions {
export function stacktracey(
stacktrace: string,
opts: StacktraceyOptions
): Promise<string> {
): Promise<string | asTableResult> {
const parseStack: Array<Promise<any>> = []
const stack = opts.preset.parseStacktrace(stacktrace)
stack.items.forEach((item, index) => {
const fn = () => {
const fn = (
item: StacktraceyItems,
index: number
): Promise<undefined | void> => {
const { line = 0, column = 0, file, fileName, fileRelative } = item
try {
if (item.thirdParty) {
return Promise.resolve()
}
function _getSourceMapContent(
file: string,
fileName: string,
fileRelative: string
) {
return opts.preset
.getSourceMapContent(file, fileName, fileRelative)
.then((content) => {
if (content) {
return getConsumer(content).then((consumer) => {
const sourceMapContent = parseSourceMapContent(consumer, {
return parseSourceMapContent(consumer, {
line,
column,
})
if (sourceMapContent) {
const {
source,
sourcePath,
sourceLine,
sourceColumn,
fileName = '',
} = sourceMapContent
stack.items[index] = Object.assign({}, item, {
file: source,
line: sourceLine,
column: sourceColumn,
fileShort: sourcePath,
fileRelative: sourcePath,
fileName,
})
}
})
}
})
}
try {
return _getSourceMapContent(file, fileName, fileRelative).then(
(sourceMapContent) => {
if (sourceMapContent) {
const {
source,
sourcePath,
sourceLine,
sourceColumn,
fileName = '',
} = sourceMapContent
stack.items[index] = Object.assign({}, item, {
file: source,
line: sourceLine,
column: sourceColumn,
fileShort: sourcePath,
fileRelative: source,
fileName,
thirdParty: isThirdParty(sourcePath),
})
/**
* 以 .js 结尾
* 包含 app-service.js 则需要再解析 两次
* 不包含 app-service.js 则无需再解析 一次
*/
const curItem = stack.items[index]
if (
(stack as StackTracey).isMP &&
curItem.beforeParse.indexOf('app-service') !== -1
) {
return fn(curItem, index)
}
}
}
)
} catch (error) {
return Promise.resolve()
}
}
parseStack.push(fn())
parseStack.push(fn(item, index))
})
return new Promise((resolve, reject) => {
......@@ -136,6 +176,10 @@ export function stacktracey(
})
}
function isThirdParty(relativePath: string) {
return relativePath.indexOf('@dcloudio') !== -1
}
function getConsumer(
content: string
): Promise<BasicSourceMapConsumer | IndexedSourceMapConsumer> {
......@@ -182,19 +226,17 @@ function getSourceMapContent(sourcemapUrl: string) {
}
}
type SourceMapContent =
| undefined
| {
source: string
sourcePath: string
sourceLine: number
sourceColumn: number
fileName: string | undefined
}
type SourceMapContent = {
source: string
sourcePath: string
sourceLine: number
sourceColumn: number
fileName: string | undefined
}
function parseSourceMapContent(
consumer: BasicSourceMapConsumer | IndexedSourceMapConsumer,
obj: Position
): SourceMapContent {
): SourceMapContent | undefined {
// source -> 'uni-app:///node_modules/@sentry/browser/esm/helpers.js'
const {
source,
......@@ -219,13 +261,32 @@ function parseSourceMapContent(
interface UniStracktraceyPresetOptions {
base: string
sourceRoot: string
splitThirdParty?: boolean
}
function joinItem(item: string[]) {
const a = item[0]
const b = item[1] ? ` ${item[1]}` : ''
const c = item[2] ? ` ${item[2]}` : ''
return `${a}${b}${c}`
}
export function uniStracktraceyPreset(
opts: UniStracktraceyPresetOptions
): StacktraceyPreset {
const { base, sourceRoot } = opts
const { base, sourceRoot, splitThirdParty } = opts
let stack: StackTracey
return {
/**
*
* 微信特殊处理
* 微信解析步骤:
* 1. //usr/app-service.js -> 'weixin/__APP__/app-service.map.map'
* 2. //usr/pages/API/app-service.js -> 'weixin/pages/API/app-service.map.map'
* 3. uni-list-item/uni-list-item.js -> ${base}/uni-list-item/uni-list-item.js.map
*/
parseSourceMapUrl(file, fileName, fileRelative) {
// 组合 sourceMapUrl
if (fileRelative.indexOf('(') !== -1)
......@@ -234,24 +295,65 @@ export function uniStracktraceyPreset(
if (sourceRoot) {
return `${fileRelative.replace(sourceRoot, base + '/')}.map`
}
return `${base}/${fileRelative}.map`
let baseAfter = ''
if (stack.isMP) {
if (fileRelative.indexOf('app-service.js') !== -1) {
baseAfter = (base.match(/\w$/) ? '/' : '') + 'weixin'
if (fileRelative === fileName) {
baseAfter += '/__APP__'
}
fileRelative = fileRelative.replace('.js', '.map')
}
if (baseAfter && !!fileRelative.match(/^\w/)) baseAfter += '/'
}
return `${base}${baseAfter}${fileRelative}.map`
},
getSourceMapContent(file, fileName, fileRelative) {
return Promise.resolve(
getSourceMapContent(
this.parseSourceMapUrl(file, fileName, fileRelative)
)
)
if (stack.isMP && fileRelative.indexOf('.js') === -1) {
return Promise.resolve('')
}
const sourcemapUrl = this.parseSourceMapUrl(file, fileName, fileRelative)
return Promise.resolve(getSourceMapContent(sourcemapUrl))
},
parseStacktrace(stacktrace) {
return new StackTracey(stacktrace)
stack = new StackTracey(stacktrace)
return stack
},
asTableStacktrace({ maxColumnWidths, stacktrace, stack }) {
const errorName = stacktrace.split('\n')[0]
return (
(errorName.indexOf('at') === -1 ? `${errorName}\n` : '') +
(stack.asTable ? stack.asTable({ maxColumnWidths }) : '')
)
const lines = stack.asTable
? stack.asTable(maxColumnWidths ? { maxColumnWidths } : undefined)
: { items: [], thirdPartyItems: [] }
if (lines.items.length || lines.thirdPartyItems.length) {
const { items: stackLines, thirdPartyItems: stackThirdPartyLines } =
lines
const userError = stack.itemsHeader
.map((item) => {
if (item === '%StacktraceyItem%') {
const _stack = stackLines.shift()
return _stack ? joinItem(_stack) : ''
}
return item
})
.filter(Boolean)
.join('\n')
const thirdParty = stackThirdPartyLines.length
? stackThirdPartyLines.map(joinItem).join('\n')
: ''
if (splitThirdParty) {
return {
userError,
thirdParty,
}
}
return userError + '\n' + thirdParty
} else {
return errorName
}
},
}
}
......@@ -312,7 +414,7 @@ export function utsStracktraceyPreset(
return undefined
}
const fileName = planA[1]
const fileName: string = planA[1]
? (planB = planA[1].match(/(.*)*\/(.+)/) || [])[2] || ''
: ''
......@@ -336,6 +438,7 @@ export function utsStracktraceyPreset(
return {
items: entries as StackTracey.Entry[],
itemsHeader: [],
}
},
asTableStacktrace({ maxColumnWidths, stacktrace, stack }) {
......
'use strict'
interface EntryMetadata {
beforeParse: string
callee: string
index: boolean
native: boolean
file: string
line: number | undefined
column: number | undefined
calleeShort?: string
}
/* ------------------------------------------------------------------------ */
const O = Object,
isBrowser =
/* eslint-disable */
typeof window !== 'undefined' &&
/* eslint-disable */
window.window === window &&
/* eslint-disable */
window.navigator,
nodeRequire = isBrowser ? null : module.require, // to prevent bundlers from expanding the require call
lastOf = (x: Array<any>) => x[x.length - 1],
nixSlashes = (x: string) => x.replace(/\\/g, '/'),
pathRoot = isBrowser ? window.location.href : nixSlashes(process.cwd()) + '/'
/* ------------------------------------------------------------------------ */
class StackTracey {
items: StackTracey.Entry[]
itemsHeader: string[] = []
isMP: boolean = false
constructor(input: string | Error | any, offset?: number) {
const originalInput = input,
isParseableSyntaxError =
input && input instanceof SyntaxError && !isBrowser
/* new StackTracey () */
if (!input) {
input = new Error()
offset = offset === undefined ? 1 : offset
}
/* new StackTracey (Error) */
if (input instanceof Error) {
input = input.stack || ''
}
/* new StackTracey (string) */
if (typeof input === 'string') {
this.isMP = input.indexOf('MiniProgramError') !== -1
input = (this.rawParse(input) as EntryMetadata[])
.slice(offset)
.map((x: EntryMetadata) => this.extractEntryMetadata(x))
}
/* new StackTracey (array) */
if (Array.isArray(input)) {
if (isParseableSyntaxError) {
const rawLines = nodeRequire!('util')
.inspect(originalInput)
.split('\n'),
fileLine = rawLines[0].split(':'),
line = fileLine.pop(),
file = fileLine.join(':')
if (file) {
input.unshift({
file: nixSlashes(file),
line: line,
column: (rawLines[2] || '').indexOf('^') + 1,
sourceLine: rawLines[1],
callee: '(syntax error)',
syntaxError: true,
})
}
}
this.items = input
} else {
this.items = []
}
}
extractEntryMetadata(e: EntryMetadata) {
const decomposedPath = this.decomposePath(e.file || '')
const fileRelative = decomposedPath[0]
const externalDomain = decomposedPath[1]
return O.assign(e, {
calleeShort: e.calleeShort || lastOf((e.callee || '').split('.')),
fileRelative: fileRelative,
fileShort: this.shortenPath(fileRelative),
fileName: lastOf((e.file || '').split('/')),
thirdParty: this.isThirdParty(fileRelative, externalDomain) && !e.index,
externalDomain: externalDomain,
})
}
shortenPath(relativePath: string) {
return relativePath
.replace(/^node_modules\//, '')
.replace(/^webpack\/bootstrap\//, '')
.replace(/^__parcel_source_root\//, '')
}
decomposePath(fullPath: string): string[] {
let result = fullPath
if (isBrowser) result = result.replace(pathRoot, '')
const externalDomainMatch = result.match(
/^(http|https)\:\/\/?([^\/]+)\/(.*)/
)
const externalDomain = externalDomainMatch
? externalDomainMatch[2]
: undefined
result = externalDomainMatch ? externalDomainMatch[3] : result
// if (!isBrowser) result = nodeRequire!('path').relative(pathRoot, result)
return [
nixSlashes(result).replace(/^.*\:\/\/?\/?/, ''), // cut webpack:/// and webpack:/ things
externalDomain!,
]
}
isThirdParty(relativePath: string, externalDomain: string) {
if (this.isMP) {
if (typeof externalDomain === 'undefined') return false
return externalDomain !== 'usr'
}
return (
externalDomain ||
relativePath[0] === '~' || // webpack-specific heuristic
relativePath[0] === '/' || // external source
relativePath.indexOf('@dcloudio') !== -1 ||
relativePath.indexOf('webpack/bootstrap') === 0
)
}
rawParse(str: string) {
const lines = (str || '').split('\n')
const entries = lines.map((line, index) => {
line = line.trim()
let callee,
fileLineColumn = [],
native,
planA,
planB
if (
(planA = line.match(/at (.+) \(eval at .+ \((.+)\), .+\)/)) || // eval calls
(planA = line.match(/at (.+) \((.+)\)/)) ||
(line.slice(0, 3) !== 'at ' && (planA = line.match(/(.*)@(.*)/)))
) {
this.itemsHeader.push('%StacktraceyItem%')
callee = planA[1]
native = planA[2] === 'native'
fileLineColumn = (
planA[2].match(/(.*):(\d+):(\d+)/) ||
planA[2].match(/(.*):(\d+)/) ||
planA[2].match(/\[(.*)\]/) ||
[]
).slice(1)
} else if ((planB = line.match(/^(at\s*)*(.*)\s+(.+):(\d+):(\d+)/))) {
this.itemsHeader.push('%StacktraceyItem%')
callee = planB[2].trim()
fileLineColumn = planB.slice(3)
} else {
this.itemsHeader.push(line)
return undefined
}
/* Detect things like Array.reduce
TODO: detect more built-in types */
if (callee && !fileLineColumn[0]) {
const type = callee.split('.')[0]
if (type === 'Array') {
native = true
}
}
return {
beforeParse: line,
callee: callee || '',
/* eslint-disable */
index: isBrowser && fileLineColumn[0] === window.location.href,
native: native || false,
file: nixSlashes(fileLineColumn[0] || ''),
line: parseInt(fileLineColumn[1] || '', 10) || undefined,
column: parseInt(fileLineColumn[2] || '', 10) || undefined,
}
})
return entries.filter((x) => x !== undefined)
}
maxColumnWidths() {
return {
callee: 30,
file: 60,
sourceLine: 80,
}
}
asTable(opts?: { maxColumnWidths: StackTracey.MaxColumnWidths }) {
const maxColumnWidths =
(opts && opts.maxColumnWidths) || this.maxColumnWidths()
const trimmed = (this as any)
.filter((e: StackTracey.Entry) => !e.thirdParty)
.map((e: StackTracey.Entry) => parseItem(e, maxColumnWidths, this.isMP))
const trimmedThirdParty = (this as any)
.filter((e: StackTracey.Entry) => e.thirdParty)
.map((e: StackTracey.Entry) => parseItem(e, maxColumnWidths, this.isMP))
return {
items: trimmed.items as Array<string[]>,
thirdPartyItems: trimmedThirdParty.items as Array<string[]>,
}
}
}
const trimEnd = (s: string, n: number) =>
s && (s.length > n ? s.slice(0, n - 1) + '' : s)
const trimStart = (s: string, n: number) =>
s && (s.length > n ? '' + s.slice(-(n - 1)) : s)
function parseItem(
e: StackTracey.Entry,
maxColumnWidths: StackTracey.MaxColumnWidths,
isMP: boolean
) {
const filePath =
(isMP ? e.file && e.file : e.fileShort && e.fileShort) +
`${e.line ? ':' + e.line : ''}` +
`${e.column ? ':' + e.column : ''}`
return [
'at ' + trimEnd(isMP ? e.callee : e.calleeShort, maxColumnWidths.callee),
trimStart(filePath || '', maxColumnWidths.file),
trimEnd((e.sourceLine || '').trim() || '', maxColumnWidths.sourceLine),
]
}
/* Array methods
------------------------------------------------------------------------ */
;['map', 'filter', 'slice', 'concat'].forEach((method) => {
;(StackTracey.prototype as any)[method] = function (/*...args */) {
// no support for ...args in Node v4 :(
return new StackTracey(this.items[method].apply(this.items, arguments))
}
})
/* ------------------------------------------------------------------------ */
export default StackTracey
declare namespace StackTracey {
interface SourceFile {
path: string
text: string
lines: string[]
error?: Error
}
interface Location {
file: string
line?: number
column?: number
}
interface Entry extends Location {
beforeParse: string
callee: string
index: boolean
native: boolean
calleeShort: string
fileRelative: string
fileShort: string
fileName: string
thirdParty: boolean
hide?: boolean
sourceLine?: string
sourceFile?: SourceFile
error?: Error
line?: number
column?: number
}
interface MaxColumnWidths {
callee: number
file: number
sourceLine: number
}
}
......@@ -20,6 +20,17 @@ Run with --stacktrace option to get the stack trace. Run with --info or --debug
BUILD FAILED in 2s
`
stacktracey(utsErrorMsg, {
preset: utsStracktraceyPreset({
base: path.resolve(
__dirname,
'./nativeplugins-sourceMap/DCloud-UTSPlugin/'
),
sourceRoot: 'DCloud-UTSPlugin/android/src/',
}),
}).then((res) => {
console.log('res :>> ', res)
})
const uniErrorMsg = `Error: Sentry Error
at a.throwError(/static/js/pages-index-index.3ab0d0e5.js:1:567)
......@@ -30,24 +41,12 @@ at ee(/static/js/chunk-vendors.75525bd5.js:34:11927)
at HTMLElement.n(/static/js/chunk-vendors.75525bd5.js:34:13824)
at HTMLElement.o._wrapper(/static/js/chunk-vendors.75525bd5.js:34:53966)
at HTMLElement.i(/static/js/chunk-vendors.75525bd5.js:7:609894)`
stacktracey(uniErrorMsg, {
preset: uniStracktraceyPreset({
base: path.resolve(__dirname, './__UNI__APPID__/1.0.0/.sourcemap/h5/'),
sourceRoot: '',
// splitThirdParty: true
}),
}).then((res) => {
console.log('res :>> ', res)
})
/* stacktracey(utsErrorMsg, {
preset: utsStracktraceyPreset({
base: path.resolve(
__dirname,
'./nativeplugins-sourceMap/DCloud-UTSPlugin/'
),
sourceRoot: 'DCloud-UTSPlugin/android/src/',
}),
}).then((res) => {
console.log('res :>> ', res)
})
*/
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册