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
feat: add error snippets #919
Conversation
b823d34
to
54cccb4
Compare
src/error-snippets/frames.ts
Outdated
import type { ResolvedFrame, SufficientStackFrame } from "./types"; | ||
|
||
// prettier-ignore | ||
const frameRelevance: Record<string, { value: number; matcher: (fileName: string) => boolean }> = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://nodejs.org/api/errors.html#errorstack
The location information will be one of:
native
, if the frame represents a call internal to V8 (as in [].forEach).plain-filename.js:line:column
, if the frame represents a call internal to Node.js./absolute/path/to/file.js:line:column
, if the frame represents a call in a user program (using CommonJS module system), or its dependencies.<transport-protocol>:///url/to/module/file.mjs:line:column
, if the frame represents a call in a user program (using ES module system), or its dependencies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ranking them as:
- 0: can't extract code snippet; useless
- 1: better than nothing
- 2: better than wdio internals, but worse, than user code part (these could be testplane parts)
- 3: user code. If frame of user code appears, always should pick it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ranking them as
this comment should be in the code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/**
* @description
* Rank values:
*
* 0: Can't extract code snippet; useless
*
* 1: WebdriverIO internals: Better than nothing
*
* 2: Project internals: Better than WebdriverIO internals, but worse, than user code part
*
* 3: User code: Best choice
*/
const FRAME_REELVANCE: Record<string, { value: number; matcher: (fileName: string) => boolean }> = {
repl: { value: 0, matcher: fileName => /^REPL\d+$/.test(fileName) },
nodeInternals: { value: 0, matcher: fileName => /^node:[a-zA-Z\-_]/.test(fileName) },
wdioInternals: { value: 1, matcher: fileName => fileName.includes("/node_modules/webdriverio/") },
projectInternals: { value: 2, matcher: fileName => fileName.includes("/node_modules/") },
userCode: { value: 3, matcher: () => true },
} as const;
} catch (findError) { | ||
logger.warn("Unable to find relevant stack frame:", findError); | ||
|
||
return null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If error appers, dont throw any error, just returning null
with console warn
src/error-snippets/index.ts
Outdated
const stackFrameResolver = async (stackFrame: SufficientStackFrame): Promise<ResolvedFrame> => { | ||
const fileContents = await getSourceCodeFile(stackFrame.fileName); | ||
const sourceMaps = await extractSourceMaps(fileContents, stackFrame.fileName); | ||
|
||
return sourceMaps ? resolveWithSourceMap(stackFrame, sourceMaps) : resolveWithStackFrame(stackFrame, fileContents); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getSourceCodeFile
extracts source code from file and applies source maps, if it is in http headers: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.lmz475t4mvbx
So in other places we dont have to think "is the file from node:fs or browser:fetch?"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
source maps will be evaluated, if exists
return sourceMaps ? resolveWithSourceMap(stackFrame, sourceMaps) : resolveWithStackFrame(stackFrame, fileContents); | ||
}; | ||
|
||
export const extendWithCodeSnippet = async (err: WithSnippetError): Promise<WithSnippetError> => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function does not throw exceptions either.
The worst it can do - log a warning into console.
? fileContents.slice(sourceMapsStartIndex + SOURCE_MAP_URL_COMMENT.length) | ||
: fileContents.slice(sourceMapsStartIndex + SOURCE_MAP_URL_COMMENT.length, sourceMapsEndIndex); | ||
|
||
const sourceMaps = await getSourceCodeFile(url.resolve(fileName, sourceMapUrl)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sourceMapUrl
could either be relative, absolute or data:application/json;base64,...
After url.resolve
we could get:
- absolute path (which we could fs.readFile)
- network url (which we could fetch)
- embedded (which would be handled by fetch)
linesBelow: number; | ||
} | ||
|
||
export const softFileURLToPath = (fileName: string): string => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For esm
, fileName
starts with file://
src/error-snippets/utils.ts
Outdated
} | ||
}; | ||
|
||
export const formatFileNameHeader = (fileName: string, opts: formatFileNameHeaderOpts): string => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formats this kind of header above code snippet:
. | // tests-dir/test.js
. |
9 | it.only('only test', async ({browser}) => {
const sourceMapUrl = response.headers.get("SourceMap"); | ||
|
||
return responseText + "\n" + SOURCE_MAP_URL_COMMENT + sourceMapUrl; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By specification, source maps could be in HTTP Header SourceMap
.
If it is, we could just append our file contents and dont think about it elsewhere
@@ -37,6 +37,11 @@ module.exports = class FlatReporter extends BaseReporter { | |||
const icon = testCase.isFailed ? icons.FAIL : icons.RETRY; | |||
|
|||
this.informer.log(` ${testCase.browserId}`); | |||
if (testCase.errorSnippet) { | |||
this.informer.log(""); | |||
testCase.errorSnippet.split("\n").forEach(line => this.informer.log(line)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
align snippet with log prefix
} | ||
|
||
if (test.err && test.err.snippet) { | ||
testInfo.errorSnippet = chalk.supportsColor ? test.err.snippet : stripAnsi(test.err.snippet); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This way we can use colorfull version for console, and remove control characters if user pipes logs into file
fe15bf2
to
be2a68f
Compare
src/error-snippets/frames.ts
Outdated
const frameRelevance: Record<string, { value: number; matcher: (fileName: string) => boolean }> = { | ||
repl: { value: 0, matcher: fileName => /^REPL\d+$/.test(fileName) }, | ||
nodeNative: { value: 0, matcher: fileName => fileName === "native" }, | ||
nodeInternals: { value: 0, matcher: fileName => /^node:[a-zA-Z\-_]/.test(fileName) || /^[a-zA-Z\-_]+\.[a-zA-Z]+$/.test(fileName)}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you, pls, provide an examples for these errors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took it from https://nodejs.org/api/errors.html#errorstack
The location information will be one of:
- native, if the frame represents a call internal to V8 (as in [].forEach).
- plain-filename.js:line:column, if the frame represents a call internal to Node.js.
- /absolute/path/to/file.js:line:column, if the frame represents a call in a user program (using CommonJS module system), or its dependencies.
- <transport-protocol>:///url/to/module/file.mjs:line:column, if the frame represents a call in a user program (using ES module system), or its dependencies.
The string representing the stack trace is lazily generated when the error.stack property is accessed.
But it looks like they forgot to update it, because internal filename differs from version to version.
In node@8 it was like this:
TypeError: Module.SourceMap is not a constructor
at myFunc (/Users/kroman512/appiumProj/stacktrace.js:12:9)
at Immediate.setImmediate [as _onImmediate] (/Users/kroman512/appiumProj/stacktrace.js:15:5)
at runCallback (timers.js:810:20)
at tryOnImmediate (timers.js:768:5)
at processImmediate [as _immediateCallback] (timers.js:745:5)
(so timers.js
is node internal)
In node@12 it was:
TypeError [ERR_INVALID_ARG_TYPE]: The "payload" argument must be of type object. Received undefined
at cloneSourceMapV3 (internal/source_map/source_map.js:315:11)
at new SourceMap (internal/source_map/source_map.js:141:21)
at myFunc (/Users/kroman512/appiumProj/stacktrace.js:12:9)
at Immediate._onImmediate (/Users/kroman512/appiumProj/stacktrace.js:15:5)
at processImmediate (internal/timers.js:461:21)
so it starts with internal/
. But starting from node@16 its this:
TypeError [ERR_INVALID_ARG_TYPE]: The "payload" argument must be of type object. Received undefined
at cloneSourceMapV3 (node:internal/source_map/source_map:318:3)
at new SourceMap (node:internal/source_map/source_map:141:21)
at myFunc (/Users/kroman512/appiumProj/stacktrace.js:12:9)
at Immediate.<anonymous> (/Users/kroman512/appiumProj/stacktrace.js:15:5)
at processImmediate (node:internal/timers:466:21)
So it is always node:internal/
in node16+
I will remove second nodeInternals
matcher as well as native
one (this one can't be produced from javascript and only can appear in node runtime itself. Also turns out, we can't parse it anyways):
/**
* @description
* Rank values:
*
* 0: Can't extract code snippet; useless
*
* 1: WebdriverIO internals: Better than nothing
*
* 2: Project internals: Better than WebdriverIO internals, but worse, than user code part
*
* 3: User code: Best choice
*/
const FRAME_REELVANCE: Record<string, { value: number; matcher: (fileName: string) => boolean }> = {
repl: { value: 0, matcher: fileName => /^REPL\d+$/.test(fileName) },
nodeInternals: { value: 0, matcher: fileName => /^node:[a-zA-Z\-_]/.test(fileName) },
wdioInternals: { value: 1, matcher: fileName => fileName.includes("/node_modules/webdriverio/") },
projectInternals: { value: 2, matcher: fileName => fileName.includes("/node_modules/") },
userCode: { value: 3, matcher: () => true },
} as const;
src/error-snippets/frames.ts
Outdated
import type { ResolvedFrame, SufficientStackFrame } from "./types"; | ||
|
||
// prettier-ignore | ||
const frameRelevance: Record<string, { value: number; matcher: (fileName: string) => boolean }> = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ranking them as
this comment should be in the code
test/src/error-snippets/frames.ts
Outdated
}); | ||
}); | ||
|
||
it("resolveWithStackFrame", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not the most understandable test name :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stackFrameResolver
-> stackFrameLocationResolver
resolveWithSourceMap
-> resolveLocationWithSourceMap
resolveWithStackFrame
-> resolveLocationWithStackFrame
assert.calledOnceWith(loggerStub.warn, "Unable to apply code snippet:"); | ||
}); | ||
|
||
it("should return the same Object.is object", async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure that the name of the test needs to be specified about Object.is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not?
{}
and {}
are the same objects. But there is a nuance)
03b2c0f
to
389eb4a
Compare
389eb4a
to
a78ba06
Compare
What is done
Add error snippets
Examples
1
2
3
Notice how stacktrace says example.hermione.tsx?import:12:18, but we are pointing to line 18 because of source map evaluation