Repository: elastic/synthetics (ts)

Issue:
improve the test stacktrace on all reporters
+ Currently the stack-trace of the runner with both default and other reporters is really minimal, It points to the error in this way
```
Journey: testing timeout issue - regular
   ---
      stack: |-
        Error: boo
            at Step.callback (/Users/vigneshh/elastic/synthetics/synth-init/journeys/debug-timeout.journey.ts:33:11)
            at runNextTicks (node:internal/process/task_queues:60:5)
            at processImmediate (node:internal/timers:447:9)
            at Runner.runStep (/Users/vigneshh/elastic/synthetics/src/core/runner.ts:216:7)
            at Runner.runSteps (/Users/vigneshh/elastic/synthetics/src/core/runner.ts:267:16)
            at Runner.runJourney (/Users/vigneshh/elastic/synthetics/src/core/runner.ts:360:27)
            at Runner.run (/Users/vigneshh/elastic/synthetics/src/core/runner.ts:453:11)
            at Command.<anonymous> (/Users/vigneshh/elastic/synthetics/src/cli.ts:138:23)
   ---

 1 failed (1319 ms)
```

This has too many information around exposing the internals of the Library and also the node internals. Instead for a user, we should only print the relevant erorr details that are simple and meaningful. 

```
Journey: testing timeout issue - regular

Error: boo

  28 | journey('testing timeout issue - regular', ({ page }) => {
  29 |   await page.goto('https://example.com/');
> 30 |   throw new Error('boo');
     |         ^
  31 | });
  32 |

    at Step.name (/Users/vigneshh/elastic/synthetics/synth-init/journeys/debug-timeout.journey.ts:33:11)
1 failed (1319 ms)
```

It has the advantage of having very less information and also has the exact snippet of code where the test is failing. 

Pull request:
feat: improve error stack with codeframes
+ fix #824 
+ This PR adds bunch of enhancements that rewrites the stacktrace for all the reporters and adds code highlighting that makes it easier to spot errors and pinpoints the exact location using babel codeframes. The Stacktrace strips all the node-internals, promise async listeners and the library internals and only adds the relevant stack information that contains the details to track down the error source. 
+ For the base reporter, the CLI nicely renders the errors using ANSI codes
<img width="801" alt="Screenshot 2023-09-26 at 5 53 51 PM" src="https://github.com/elastic/synthetics/assets/3902525/3dcf1088-ed1b-46b0-8d5b-33f05c0d4aed">

+ For the JSON reporter which HB uses and displayed on the Kibana, we are not displaying the source and the source is highlighted by the `payload.source` and displayed in the next tab. Plus, the sourcemap during bundling gets a bit different as the new lines are by default stripped out from the bundle which leads to off by 1 line source highlight issue. 
<img width="776" alt="Screenshot 2023-09-26 at 6 00 12 PM" src="https://github.com/elastic/synthetics/assets/3902525/c1eea7c1-fae8-4b60-8bc5-588e1211e475">
 

Patch:
diff --git a/package-lock.json b/package-lock.json
index b263f26..f5ad21f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
       "version": "0.0.0-development",
       "license": "MIT",
       "dependencies": {
+        "@babel/code-frame": "^7.22.13",
         "archiver": "^5.3.1",
         "commander": "^10.0.1",
         "deepmerge": "^4.3.1",
@@ -26,6 +27,7 @@
         "snakecase-keys": "^4.0.1",
         "sonic-boom": "^3.3.0",
         "source-map-support": "^0.5.21",
+        "stack-utils": "^2.0.6",
         "undici": "^5.21.2",
         "yaml": "^2.2.2"
       },
@@ -37,11 +39,13 @@
         "@commitlint/cli": "^17.7.1",
         "@commitlint/config-conventional": "^17.7.0",
         "@semantic-release/exec": "^6.0.3",
+        "@types/babel__code-frame": "^7.0.3",
         "@types/jest": "^28.1.8",
         "@types/micromatch": "^4.0.2",
         "@types/node": "^16.11.59",
         "@types/semver": "^7",
         "@types/sharp": "^0.28.6",
+        "@types/stack-utils": "^2.0.1",
         "@typescript-eslint/eslint-plugin": "^5.38.0",
         "@typescript-eslint/parser": "^5.38.0",
         "eslint": "^8.23.1",
@@ -75,17 +79,81 @@
       }
     },
     "node_modules/@babel/code-frame": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz",
-      "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==",
-      "dev": true,
+      "version": "7.22.13",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+      "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
       "dependencies": {
-        "@babel/highlight": "^7.22.5"
+        "@babel/highlight": "^7.22.13",
+        "chalk": "^2.4.2"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@babel/code-frame/node_modules/ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dependencies": {
+        "color-convert": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/@babel/code-frame/node_modules/chalk": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "dependencies": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/@babel/code-frame/node_modules/color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "dependencies": {
+        "color-name": "1.1.3"
+      }
+    },
+    "node_modules/@babel/code-frame/node_modules/color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+    },
+    "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/@babel/code-frame/node_modules/has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/@babel/code-frame/node_modules/supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/@babel/compat-data": {
       "version": "7.22.6",
       "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz",
@@ -288,10 +356,9 @@
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz",
-      "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==",
-      "dev": true,
+      "version": "7.22.20",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
       "engines": {
         "node": ">=6.9.0"
       }
@@ -320,13 +387,12 @@
       }
     },
     "node_modules/@babel/highlight": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz",
-      "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==",
-      "dev": true,
+      "version": "7.22.20",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
+      "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.22.5",
-        "chalk": "^2.0.0",
+        "@babel/helper-validator-identifier": "^7.22.20",
+        "chalk": "^2.4.2",
         "js-tokens": "^4.0.0"
       },
       "engines": {
@@ -337,7 +403,6 @@
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
       "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
       "dependencies": {
         "color-convert": "^1.9.0"
       },
@@ -349,7 +414,6 @@
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
       "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^3.2.1",
         "escape-string-regexp": "^1.0.5",
@@ -363,7 +427,6 @@
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
       "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
       "dependencies": {
         "color-name": "1.1.3"
       }
@@ -371,14 +434,12 @@
     "node_modules/@babel/highlight/node_modules/color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "dev": true
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
     },
     "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
       "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "dev": true,
       "engines": {
         "node": ">=0.8.0"
       }
@@ -387,7 +448,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
       "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "dev": true,
       "engines": {
         "node": ">=4"
       }
@@ -396,7 +456,6 @@
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
       "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dev": true,
       "dependencies": {
         "has-flag": "^3.0.0"
       },
@@ -3324,6 +3383,12 @@
       "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
       "dev": true
     },
+    "node_modules/@types/babel__code-frame": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.0.4.tgz",
+      "integrity": "sha512-WBxINLlATjvmpCgBbb9tOPrKtcPfu4A/Yz2iRzmdaodfvjAS/Z0WZJClV9/EXvoC9viI3lgUs7B9Uo7G/RmMGg==",
+      "dev": true
+    },
     "node_modules/@types/babel__core": {
       "version": "7.20.1",
       "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz",
@@ -3475,9 +3540,9 @@
       }
     },
     "node_modules/@types/stack-utils": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
-      "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
+      "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
       "dev": true
     },
     "node_modules/@types/yargs": {
@@ -5255,7 +5320,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
       "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
@@ -8319,8 +8383,7 @@
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-      "dev": true
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
     },
     "node_modules/js-yaml": {
       "version": "3.14.1",
@@ -14349,10 +14412,9 @@
       "dev": true
     },
     "node_modules/stack-utils": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz",
-      "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==",
-      "dev": true,
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
+      "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
       "dependencies": {
         "escape-string-regexp": "^2.0.0"
       },
diff --git a/package.json b/package.json
index 18bb773..5a38a9b 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
   "author": "",
   "license": "MIT",
   "dependencies": {
+    "@babel/code-frame": "^7.22.13",
     "archiver": "^5.3.1",
     "commander": "^10.0.1",
     "deepmerge": "^4.3.1",
@@ -58,6 +59,7 @@
     "snakecase-keys": "^4.0.1",
     "sonic-boom": "^3.3.0",
     "source-map-support": "^0.5.21",
+    "stack-utils": "^2.0.6",
     "undici": "^5.21.2",
     "yaml": "^2.2.2"
   },
@@ -65,11 +67,13 @@
     "@commitlint/cli": "^17.7.1",
     "@commitlint/config-conventional": "^17.7.0",
     "@semantic-release/exec": "^6.0.3",
+    "@types/babel__code-frame": "^7.0.3",
     "@types/jest": "^28.1.8",
     "@types/micromatch": "^4.0.2",
     "@types/node": "^16.11.59",
     "@types/semver": "^7",
     "@types/sharp": "^0.28.6",
+    "@types/stack-utils": "^2.0.1",
     "@typescript-eslint/eslint-plugin": "^5.38.0",
     "@typescript-eslint/parser": "^5.38.0",
     "eslint": "^8.23.1",
diff --git a/src/common_types.ts b/src/common_types.ts
index 35cb7b2..1c9f9e4 100644
--- a/src/common_types.ts
+++ b/src/common_types.ts
@@ -43,6 +43,10 @@ export type Location = {
   column: number;
 };
 
+export type StackFrame = Location & {
+  function?: string;
+};
+
 export type Params = Record<string, any>;
 export type HooksArgs = {
   env: string;
@@ -274,6 +278,14 @@ export type JourneyResult = {
   browserconsole?: PluginOutput['browserconsole'];
 };
 
+export type TestError = {
+  message: string;
+  stack?: string;
+  location?: Location;
+  // source location highlighting the error source
+  source?: string;
+};
+
 export type StepResult = {
   status: StatusValue;
   url?: string;
diff --git a/src/core/runner.ts b/src/core/runner.ts
index 0996d93..f7d67cf 100644
--- a/src/core/runner.ts
+++ b/src/core/runner.ts
@@ -212,8 +212,9 @@ export default class Runner {
        */
       pluginManager.onStep(step);
       traceEnabled && (await pluginManager.start('trace'));
-      // call the step definition
-      await step.callback();
+      // invoke the step callback by extracting to a variable to get better stack trace
+      const cb = step.callback;
+      await cb();
     } catch (error) {
       data.status = 'failed';
       data.error = error;
@@ -291,8 +292,7 @@ export default class Runner {
       params,
     });
     /**
-     * Exeucute the journey callback which would
-     * register the steps for the current journey
+     * Exeucute the journey callback which registers the steps for current journey
      */
     journey.callback({ ...context.driver, params });
   }
diff --git a/src/helpers.ts b/src/helpers.ts
index bdf1763..d9b892b 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -234,30 +234,6 @@ export function rewriteErrorStack(stack: string, indexes: [number, number]) {
   return stack;
 }
 
-// formatError prefers receiving proper Errors, but since at runtime
-// non Error exceptions can be thrown, it tolerates though. The
-// redundant type Error | any expresses that.
-export function formatError(error: Error | any) {
-  if (error === undefined || error === null) {
-    return;
-  }
-
-  if (!(error instanceof Error)) {
-    return {
-      message: `Error "${error}" received, with type "${typeof error}". (Do not throw exceptions without using \`new Error("my message")\`)`,
-      name: '',
-      stack: '',
-    };
-  }
-  const { name, message, stack } = error;
-  const indexes = findPWLogsIndexes(message);
-  return {
-    name,
-    message: rewriteErrorMessage(message, indexes[0]),
-    stack: rewriteErrorStack(stack, indexes),
-  };
-}
-
 const cwd = process.cwd();
 /**
  * All the settings that are related to the Synthetics is stored
diff --git a/src/reporters/base.ts b/src/reporters/base.ts
index f8967f3..e6b43d8 100644
--- a/src/reporters/base.ts
+++ b/src/reporters/base.ts
@@ -24,14 +24,9 @@
  */
 
 import SonicBoom from 'sonic-boom';
-import { green, red, cyan } from 'kleur/colors';
-import {
-  symbols,
-  indent,
-  now,
-  findPWLogsIndexes,
-  rewriteErrorStack,
-} from '../helpers';
+import { green, red, cyan, gray } from 'kleur/colors';
+import { renderError, serializeError } from './reporter-util';
+import { symbols, indent, now } from '../helpers';
 import { Reporter, ReporterOptions } from '.';
 import {
   JourneyEndResult,
@@ -40,25 +35,6 @@ import {
 } from '../common_types';
 import { Journey, Step } from '../dsl';
 
-function renderError(error) {
-  let output = '';
-  const outer = indent('');
-  const inner = indent(outer);
-  const container = outer + '---\n';
-  output += container;
-  let stack = error.stack;
-  if (stack) {
-    output += inner + 'stack: |-\n';
-    stack = rewriteErrorStack(stack, findPWLogsIndexes(stack));
-    const lines = String(stack).split('\n');
-    for (const line of lines) {
-      output += inner + '  ' + line + '\n';
-    }
-  }
-  output += container;
-  return red(output);
-}
-
 function renderDuration(durationMs) {
   return parseInt(durationMs);
 }
@@ -96,11 +72,14 @@ export default class BaseReporter implements Reporter {
 
   onStepEnd(_: Journey, step: Step, result: StepEndResult) {
     const { status, end, start, error } = result;
-    const message = `${symbols[status]}  Step: '${
-      step.name
-    }' ${status} (${renderDuration((end - start) * 1000)} ms)`;
+    const message = `${symbols[status]}  Step: '${step.name}' ${status} ${gray(
+      '(' + renderDuration((end - start) * 1000) + ' ms)'
+    )}`;
     this.write(indent(message));
-    error && this.write(renderError(error));
+    if (error) {
+      const message = renderError(serializeError(error));
+      this.write(indent(message) + '\n');
+    }
     this.metrics[status]++;
   }
 
@@ -113,7 +92,8 @@ export default class BaseReporter implements Reporter {
      * be executed which means the error happened in one of the hooks
      */
     if (total === 0 && error) {
-      this.write(renderError(error));
+      const message = renderError(serializeError(error));
+      this.write(indent(message) + '\n');
     }
   }
 
diff --git a/src/reporters/json.ts b/src/reporters/json.ts
index d68b371..54e704d 100644
--- a/src/reporters/json.ts
+++ b/src/reporters/json.ts
@@ -28,8 +28,8 @@ import { join } from 'path';
 import sharp from 'sharp';
 import { createHash } from 'crypto';
 import BaseReporter from './base';
+import { renderError, serializeError, stripAnsiCodes } from './reporter-util';
 import {
-  formatError,
   getTimestamp,
   CACHE_PATH,
   totalist,
@@ -189,6 +189,36 @@ export function formatNetworkFields(network: NetworkInfo) {
   return { ecs, payload };
 }
 
+/**
+ * formatJSONError formats the error in a structured format
+ * we restructure the error with code frame and stack trace for test errors
+ */
+function formatJSONError(error: Error | any, type: OutputType) {
+  if (error == null) {
+    return;
+  }
+
+  /**
+   * Do not process browser errors - console.error and unhandled exceptions
+   */
+  if (type != null && type === 'journey/browserconsole') {
+    return {
+      name: error.name,
+      message: error.message,
+      stack: error.stack,
+    };
+  }
+
+  /**
+   * Do not highlight source for these errors and strip ANSI codes
+   */
+  return {
+    name: error.name,
+    message: stripAnsiCodes(error.message.split('\n')[0]),
+    stack: stripAnsiCodes(renderError(serializeError(error, false))),
+  };
+}
+
 function journeyInfo(
   journey: OutputFields['journey'],
   type: OutputFields['type'],
@@ -550,7 +580,7 @@ export default class JSONReporter extends BaseReporter {
       payload,
       blob,
       blob_mime,
-      error: formatError(error),
+      error: formatJSONError(error, type),
       url,
       package_version: version,
     });
diff --git a/src/reporters/junit.ts b/src/reporters/junit.ts
index 1fad13a..7140add 100644
--- a/src/reporters/junit.ts
+++ b/src/reporters/junit.ts
@@ -31,8 +31,9 @@ import {
   StepEndResult,
 } from '../common_types';
 import { Journey, Step } from '../dsl';
-import { formatError, indent, now } from '../helpers';
+import { indent, now } from '../helpers';
 import BaseReporter from './base';
+import { serializeError, stripAnsiCodes } from './reporter-util';
 
 type XMLEntry = {
   name: string;
@@ -97,14 +98,13 @@ export default class JUnitReporter extends BaseReporter {
 
     entry.attributes.tests++;
     if (status === 'failed') {
-      const { name, message, stack } = formatError(error);
       caseEntry.children.push({
         name: 'failure',
         attributes: {
-          message,
-          type: name,
+          message: error.message,
+          type: error.name,
         },
-        text: stack,
+        text: stripAnsiCodes(serializeError(error).stack),
       });
       entry.attributes.failures++;
     } else if (status === 'skipped') {
diff --git a/src/reporters/reporter-util.ts b/src/reporters/reporter-util.ts
new file mode 100644
index 0000000..35dea9d
--- /dev/null
+++ b/src/reporters/reporter-util.ts
@@ -0,0 +1,217 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2020-present, Elastic NV
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+import { fileURLToPath } from 'url';
+import { resolve, sep } from 'path';
+import { readFileSync } from 'fs';
+import { inspect } from 'util';
+import { gray } from 'kleur/colors';
+import { codeFrameColumns } from '@babel/code-frame';
+import StackUtils from 'stack-utils';
+import { Location, StackFrame, TestError } from '../common_types';
+
+const stackUtils = new StackUtils({ internals: StackUtils.nodeInternals() });
+
+const SYNTHETICS_PATH = resolve(__dirname, '..', '..');
+const PW_CORE_PATH = require.resolve('playwright-core');
+
+function prepareStackFrame(line: string): StackFrame {
+  const frame = stackUtils.parseLine(line);
+  if (!frame) {
+    return;
+  }
+  // ignore node internals
+  if (frame.file?.startsWith('internal') || frame.file?.startsWith('node:')) {
+    return;
+  }
+
+  // handle relative URLs
+  let file = frame.file?.startsWith('file://')
+    ? fileURLToPath(frame.file)
+    : resolve(process.cwd(), frame.file);
+
+  // ignore node_modules
+  if (file.includes(`${sep}node_modules${sep}`)) {
+    return;
+  }
+
+  // filter library and PW files
+  if (!filterLibInternals(file)) {
+    return;
+  }
+
+  // When we bundle the journeys, we store the absolute path of the journey
+  // files and write the sourcemap relative to these files. When we try to
+  // extract the sourcemap file location, the stacktrace will be relatively
+  // resolved to these files which would be under `journeys` folder. So we strip
+  // these extra `journeys` from the path to get the correct file location.
+  if (frame.file?.includes('journeys/journeys')) {
+    file = file.replace('journeys/journeys', 'journeys');
+  }
+
+  return {
+    file,
+    line: frame.line || 0,
+    column: frame.column || 0,
+    function: frame.function,
+  };
+}
+
+function filterLibInternals(file: string) {
+  // To ignore filtering the stack trace on tests
+  if (process.env.TEST_OVERRIDE) {
+    return true;
+  }
+  if (file.startsWith(SYNTHETICS_PATH)) {
+    return false;
+  }
+  // should have been filtered by node_modules, but just in case
+  if (file.startsWith(PW_CORE_PATH)) {
+    return false;
+  }
+}
+
+function constructStackFromFrames(frames: StackFrame[]) {
+  const stackLines: string[] = [];
+  for (const frame of frames) {
+    stackLines.push(
+      `    at ${frame.function ? frame.function + ' ' : ''}(${frame.file}:${
+        frame.line
+      }:${frame.column})`
+    );
+  }
+  return stackLines;
+}
+
+export function prepareError(error: Error) {
+  const stack = error.stack;
+  const lines = stack.split('\n');
+  let startAt = lines.findIndex(line => line.startsWith('    at '));
+  if (startAt === -1) {
+    startAt = lines.length;
+  }
+  const message = lines.slice(0, startAt).join('\n');
+  const stackLines = lines.slice(startAt);
+  // figure out the location of the journey/step that throws the error and
+  // correct stack line corresponding to that error
+  const stackFrames: StackFrame[] = [];
+  for (const line of stackLines) {
+    const frame = prepareStackFrame(line);
+    if (!frame || !frame.file) {
+      continue;
+    }
+    stackFrames.push(frame);
+  }
+
+  let location: Location | undefined;
+  if (stackFrames.length) {
+    const frame = stackFrames[0];
+    location = {
+      file: frame.file,
+      column: frame.column || 0,
+      line: frame.line || 0,
+    };
+  }
+
+  return {
+    message,
+    stack: constructStackFromFrames(stackFrames).join('\n'),
+    location,
+  };
+}
+
+export function highLightSource(location: Location): string {
+  if (!location) {
+    return;
+  }
+
+  try {
+    const source = readFileSync(location.file, 'utf-8');
+    const code = codeFrameColumns(
+      source,
+      { start: location },
+      { highlightCode: true }
+    );
+    return code;
+  } catch (_) {
+    // ignore error
+  }
+}
+
+// serializeError prefers receiving proper Errors, but since at runtime
+// non Error exceptions can be thrown, it tolerates though. The
+// redundant type Error | any expresses that.
+export function serializeError(
+  error: Error | any,
+  highlightCode = true
+): TestError {
+  if (!(error instanceof Error) || !error.stack) {
+    return { message: `thrown: ${inspect(error)}` };
+  }
+
+  const testErr: TestError = { message: error.message };
+  const { message, stack, location } = prepareError(error);
+  testErr.message = message;
+  if (stack) {
+    testErr.stack = stack;
+  }
+  if (location && highlightCode) {
+    testErr.location = location;
+    testErr.source = highLightSource(testErr.location);
+  }
+  return testErr;
+}
+
+export function renderError(error: TestError) {
+  const summary = [];
+  // push empty string to add a new line before rendering error
+  summary.push('');
+  summary.push(error.message);
+
+  if (error.source) {
+    summary.push('');
+    summary.push(error.source);
+  }
+
+  if (error.stack) {
+    summary.push('');
+    summary.push(gray(error.stack));
+  }
+  summary.join('');
+
+  return summary.join('\n');
+}
+
+// strip color codes and ansi escape codes
+const ansiRegex = new RegExp(
+  [
+    '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
+    '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
+  ].join('|'),
+  'g'
+);
+export function stripAnsiCodes(msg = '') {
+  return msg.replace(ansiRegex, '');
+}


Test patch:
diff --git a/__tests__/cli.test.ts b/__tests__/cli.test.ts
index 69a487a..eabea83 100644
--- a/__tests__/cli.test.ts
+++ b/__tests__/cli.test.ts
@@ -449,9 +449,9 @@ describe('CLI', () => {
         ])
         .run();
       await cli.waitFor('step/end');
-      const output = cli.output();
+      const [output] = safeNDJSONParse(cli.output());
       expect(await cli.exitCode).toBe(0);
-      expect(JSON.parse(output).step).toMatchObject({
+      expect(output.step).toMatchObject({
         status: 'succeeded',
       });
     });
diff --git a/__tests__/helper.test.ts b/__tests__/helper.test.ts
index f8f4d02..c149d3b 100644
--- a/__tests__/helper.test.ts
+++ b/__tests__/helper.test.ts
@@ -27,7 +27,6 @@ import { cwd } from 'process';
 import {
   indent,
   monotonicTimeInSeconds,
-  formatError,
   findPkgJsonByTraversing,
   generateTempPath,
   rewriteErrorStack,
@@ -58,35 +57,6 @@ it('convert trace timestamp to internal time', () => {
   expect(elapsedTime).toBe(392583.998697);
 });
 
-describe('formatting errors', () => {
-  it('formats proper errors', () => {
-    const error = new Error('testing');
-    const { name, message, stack } = error;
-    const formatted = formatError(error);
-    expect(formatted).toStrictEqual({
-      name,
-      message,
-      stack,
-    });
-  });
-
-  ["c'est ne pas une Error", 42, { an: 'object' }].forEach(obj => {
-    it(`formats thrown non-error errors like: ${obj}(${typeof obj})`, () => {
-      const formatted = formatError(obj) as Error;
-      expect(formatted.message).toContain(`${obj}`);
-      expect(formatted.name).toStrictEqual('');
-      expect(formatted.stack).toStrictEqual('');
-      expect(typeof formatted.message).toBe('string');
-    });
-  });
-
-  [null, undefined].forEach(obj => {
-    it(`returns undefined for ${typeof obj}`, () => {
-      expect(formatError(obj)).toBe(undefined);
-    });
-  });
-});
-
 it('throw error when no package.json found', async () => {
   try {
     const tempPath = generateTempPath();
diff --git a/__tests__/reporters/__snapshots__/base.test.ts.snap b/__tests__/reporters/__snapshots__/base.test.ts.snap
index 2d220b3..190e90f 100644
--- a/__tests__/reporters/__snapshots__/base.test.ts.snap
+++ b/__tests__/reporters/__snapshots__/base.test.ts.snap
@@ -3,10 +3,8 @@
 exports[`base reporter render hook errors without steps 1`] = `
 "
 Journey: j1
-   ---
-      stack: |-
-        Error: before hook failed
-   ---
+   
+   Error: before hook failed
 
 No tests found! (0 ms) 
 
@@ -17,10 +15,8 @@ exports[`base reporter writes each step to the FD 1`] = `
 "
 Journey: j1
    ✖  Step: 's1' failed (1000 ms)
-   ---
-      stack: |-
-        Error: step failed
-   ---
+   
+   Error: step failed
 
 
  1 failed (0 ms) 
diff --git a/__tests__/reporters/__snapshots__/junit.test.ts.snap b/__tests__/reporters/__snapshots__/junit.test.ts.snap
index 0137d55..be3a4b2 100644
--- a/__tests__/reporters/__snapshots__/junit.test.ts.snap
+++ b/__tests__/reporters/__snapshots__/junit.test.ts.snap
@@ -16,7 +16,6 @@ exports[`junit reporter writes the output to fd 1`] = `
    <testsuite name="j1" tests="2" failures="1" skipped="1" errors="0">
       <testcase name="s1" classname="j1 s1" time="1">
          <failure message="Boom" type="Error">
-         at /__tests/reporters/junit.test.ts
          </failure>
       </testcase>
       <testcase name="s2" classname="j1 s2" time="1">
diff --git a/__tests__/reporters/__snapshots__/reporter-utils.test.ts.snap b/__tests__/reporters/__snapshots__/reporter-utils.test.ts.snap
new file mode 100644
index 0000000..9241424
--- /dev/null
+++ b/__tests__/reporters/__snapshots__/reporter-utils.test.ts.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Reporter utils render error and highlight source 1`] = `
+"  72 |
+  73 |   it('render error and highlight source', () => {
+> 74 |     const error = new Error('test error');
+     |                   ^
+  75 |     const err = serializeError(error);
+  76 |     expect(stripAnsiCodes(err.source)).toMatchSnapshot();
+  77 |   });"
+`;
diff --git a/__tests__/reporters/base.test.ts b/__tests__/reporters/base.test.ts
index 7e9bec8..d58aab5 100644
--- a/__tests__/reporters/base.test.ts
+++ b/__tests__/reporters/base.test.ts
@@ -33,6 +33,7 @@ import SonicBoom from 'sonic-boom';
 import { step, journey } from '../../src/core';
 import BaseReporter from '../../src/reporters/base';
 import * as helpers from '../../src/helpers';
+import { Step } from '../../src/dsl';
 
 describe('base reporter', () => {
   let dest: string;
@@ -69,13 +70,9 @@ describe('base reporter', () => {
     reporter.onJourneyStart(j1, {
       timestamp,
     });
-    reporter.onStepEnd(j1, step('s1', helpers.noop), {
+    reporter.onStepEnd(j1, step('s1', helpers.noop) as Step, {
       status: 'failed',
-      error: {
-        name: 'Error',
-        message: 'step failed',
-        stack: 'Error: step failed',
-      },
+      error: new Error('step failed'),
       url: 'dummy',
       start: 0,
       end: 1,
@@ -88,11 +85,7 @@ describe('base reporter', () => {
     reporter.onJourneyStart(j1, {
       timestamp,
     });
-    const error = {
-      name: 'Error',
-      message: 'before hook failed',
-      stack: 'Error: before hook failed',
-    };
+    const error = new Error('before hook failed');
     reporter.onJourneyEnd(j1, {
       timestamp,
       status: 'failed',
diff --git a/__tests__/reporters/json.test.ts b/__tests__/reporters/json.test.ts
index cf88cc4..f5cf62d 100644
--- a/__tests__/reporters/json.test.ts
+++ b/__tests__/reporters/json.test.ts
@@ -36,6 +36,7 @@ import JSONReporter, {
 import * as helpers from '../../src/helpers';
 import { NETWORK_INFO } from '../fixtures/networkinfo';
 import { StatusValue } from '../../src/common_types';
+import { Step } from '../../src/dsl';
 
 /**
  * Mock package version to avoid breaking JSON payload
@@ -107,7 +108,7 @@ describe('json reporter', () => {
     reporter.onJourneyStart(j1, {
       timestamp,
     });
-    reporter.onStepEnd(j1, step('s1', helpers.noop), {
+    reporter.onStepEnd(j1, step('s1', helpers.noop) as Step, {
       status: 'succeeded',
       url: 'dummy',
       start: 0,
@@ -196,7 +197,7 @@ describe('json reporter', () => {
   it('writes step errors to the top level', async () => {
     const myErr = new Error('myError');
 
-    reporter.onStepEnd(j1, step('s2', helpers.noop), {
+    reporter.onStepEnd(j1, step('s2', helpers.noop) as Step, {
       status: 'failed',
       url: 'dummy2',
       start: 11,
@@ -207,7 +208,7 @@ describe('json reporter', () => {
     const stepEnd = (await readAndCloseStreamJson()).find(
       json => json.type == 'step/end'
     );
-    expect(stepEnd.error).toEqual(helpers.formatError(myErr));
+    expect(stepEnd.error.stack).toContain(`Error: myError`);
   });
 
   it('writes journey errors to the top level', async () => {
@@ -225,7 +226,7 @@ describe('json reporter', () => {
     const journeyEnd = (await readAndCloseStreamJson()).find(
       json => json.type == 'journey/end'
     );
-    expect(journeyEnd.error).toEqual(helpers.formatError(myErr));
+    expect(journeyEnd.error.stack).toContain(`Error: myError`);
   });
 
   it('writes full journey info if present', async () => {
diff --git a/__tests__/reporters/junit.test.ts b/__tests__/reporters/junit.test.ts
index bb8b1da..07992ea 100644
--- a/__tests__/reporters/junit.test.ts
+++ b/__tests__/reporters/junit.test.ts
@@ -29,6 +29,7 @@ import SonicBoom from 'sonic-boom';
 import { step, journey } from '../../src/core';
 import JUnitReporter from '../../src/reporters/junit';
 import * as helpers from '../../src/helpers';
+import { Step } from '../../src/dsl';
 
 describe('junit reporter', () => {
   beforeEach(() => {});
@@ -37,7 +38,7 @@ describe('junit reporter', () => {
   let reporter: JUnitReporter;
   const timestamp = 1600300800000000;
   const j1 = journey('j1', () => {});
-  const s1 = step('s1', () => {});
+  const s1 = step('s1', () => {}) as Step;
 
   beforeEach(() => {
     dest = helpers.generateTempPath();
@@ -53,18 +54,14 @@ describe('junit reporter', () => {
       timestamp,
     });
     const error = new Error('Boom');
-    /**
-     * Snapshots would be different for everyone
-     * so keep it simple
-     */
-    error.stack = 'at /__tests/reporters/junit.test.ts';
+
     reporter.onStepEnd(j1, s1, {
       status: 'failed',
       error,
       start: 0,
       end: 1,
     });
-    reporter.onStepEnd(j1, step('s2', helpers.noop), {
+    reporter.onStepEnd(j1, step('s2', helpers.noop) as Step, {
       status: 'skipped',
       start: 0,
       end: 1,
diff --git a/__tests__/reporters/reporter-utils.test.ts b/__tests__/reporters/reporter-utils.test.ts
new file mode 100644
index 0000000..fc0f6d7
--- /dev/null
+++ b/__tests__/reporters/reporter-utils.test.ts
@@ -0,0 +1,78 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2020-present, Elastic NV
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+import {
+  prepareError,
+  serializeError,
+  stripAnsiCodes,
+} from '../../src/reporters/reporter-util';
+
+beforeAll(() => {
+  process.env['TEST_OVERRIDE'] = 'true';
+});
+afterAll(() => {
+  process.env['TEST_OVERRIDE'] = undefined;
+});
+
+describe('Reporter utils', () => {
+  const error = new Error('test error');
+  error.stack = `Error: test error
+    at Step.callback (/tmp/elastic-synthetics-unzip-3180424903/journeys/journeys/errors.journey.ts:15:11)
+    at runNextTicks (node:internal/process/task_queues:60:5)
+    at processImmediate (node:internal/timers:447:9)
+    at Runner.runStep (/usr/share/heartbeat/.node/node/lib/node_modules/@elastic/synthetics/src/core/runner.ts:213:7)
+    at Runner.runSteps (/usr/share/heartbeat/.node/node/lib/node_modules/@elastic/synthetics/src/core/runner.ts:263:16)
+    at Runner.runJourney (/usr/share/heartbeat/.node/node/lib/node_modules/@elastic/synthetics/src/core/runner.ts:353:27)
+    at Runner.run (/usr/share/heartbeat/.node/node/lib/node_modules/@elastic/synthetics/src/core/runner.ts:446:11)
+    at Command.<anonymous> (/usr/share/heartbeat/.node/node/lib/node_modules/@elastic/synthetics/src/cli.ts:138:23)
+  `;
+
+  it('prepare error', () => {
+    const err = prepareError(error);
+    expect(err.message).toBe('Error: test error');
+    expect(err).toEqual({
+      message: 'Error: test error',
+      stack:
+        '    at Step.callback (/tmp/elastic-synthetics-unzip-3180424903/journeys/errors.journey.ts:15:11)',
+      location: {
+        file: '/tmp/elastic-synthetics-unzip-3180424903/journeys/errors.journey.ts',
+        column: 11,
+        line: 15,
+      },
+    });
+  });
+
+  it('serialize error', () => {
+    const error = new Error('test error');
+    const err = serializeError(error, false);
+    expect(err.source).toBeUndefined();
+  });
+
+  it('render error and highlight source', () => {
+    const error = new Error('test error');
+    const err = serializeError(error);
+    expect(stripAnsiCodes(err.source)).toMatchSnapshot();
+  });
+});