diff --git a/.eslintrc.json b/.eslintrc.json
index 578f120e8f..7eb95650fb 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -427,6 +427,7 @@
"no-magic-numbers": "off",
"semi": "off",
"no-unused-vars": "off",
+ "no-useless-constructor": "off",
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "error",
@@ -451,7 +452,7 @@
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-for-in-array": "off",
"@typescript-eslint/no-inferrable-types": "error",
- "@typescript-eslint/no-magic-numbers": "error",
+ "@typescript-eslint/no-magic-numbers": "off",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "off",
diff --git a/frontend_tests/node_tests/blueslip_stacktrace.js b/frontend_tests/node_tests/blueslip_stacktrace.js
new file mode 100644
index 0000000000..b8f6632af7
--- /dev/null
+++ b/frontend_tests/node_tests/blueslip_stacktrace.js
@@ -0,0 +1,56 @@
+const blueslip_stacktrace = zrequire("blueslip_stacktrace");
+
+run_test("clean_path", () => {
+ // Local file
+ assert.strictEqual(
+ blueslip_stacktrace.clean_path("webpack:///static/js/upload.js"),
+ "/static/js/upload.js"
+ );
+
+ // Third party library (jQuery)
+ assert.strictEqual(
+ blueslip_stacktrace.clean_path(
+ "webpack:///.-npm-cache/de76fb6f582a29b053274f9048b6158091351048/node_modules/jquery/dist/jquery.js"
+ ),
+ "jquery/dist/jquery.js"
+ );
+
+ // Third party library (underscore)
+ assert.strictEqual(
+ blueslip_stacktrace.clean_path(
+ "webpack:///.-npm-cache/de76fb6f582a29b053274f9048b…58091351048/node_modules/underscore/underscore.js"
+ ),
+ "underscore/underscore.js"
+ );
+});
+
+run_test("clean_function_name", () => {
+ assert.deepEqual(blueslip_stacktrace.clean_function_name(undefined), undefined);
+
+ // Local file
+ assert.deepEqual(
+ blueslip_stacktrace.clean_function_name("Object../static/js/upload.js.exports.options"),
+ {
+ scope: "Object../static/js/upload.js.exports.",
+ name: "options",
+ }
+ );
+
+ // Third party library (jQuery)
+ assert.deepEqual(blueslip_stacktrace.clean_function_name("mightThrow"), {
+ scope: "",
+ name: "mightThrow",
+ });
+
+ // Third party library (underscore)
+ assert.deepEqual(
+ blueslip_stacktrace.clean_function_name(
+ "Function.../zulip-npm-cache/de76fb6f582a29b053274f…es/underscore/underscore.js?3817._.each._.forEach"
+ ),
+ {
+ scope:
+ "Function.../zulip-npm-cache/de76fb6f582a29b053274f…es/underscore/underscore.js?3817._.each._.",
+ name: "forEach",
+ }
+ );
+});
diff --git a/package.json b/package.json
index 1d9c17b50a..890960fe0d 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
"postcss-nested": "^4.1.2",
"postcss-scss": "^2.0.0",
"postcss-simple-vars": "^5.0.2",
+ "regenerator-runtime": "^0.13.3",
"script-loader": "^0.7.2",
"shebang-loader": "^0.0.1",
"simplebar": "^4.1.0",
@@ -55,6 +56,7 @@
"sorttable": "^1.0.2",
"source-sans-pro": "^3.6.0",
"spectrum-colorpicker": "^1.8.0",
+ "stacktrace-gps": "^3.0.3",
"style-loader": "^1.0.0",
"terser-webpack-plugin": "^2.1.0",
"to-markdown": "^3.1.0",
@@ -69,6 +71,7 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
+ "@types/jquery": "^3.3.31",
"@types/node": "^12.0.7",
"@types/underscore": "^1.8.18",
"@types/webpack": "^4.4.32",
diff --git a/static/js/blueslip.js b/static/js/blueslip.js
index affa18d761..31c33d6ca7 100644
--- a/static/js/blueslip.js
+++ b/static/js/blueslip.js
@@ -4,6 +4,8 @@
// in order to be able to report exceptions that occur during their
// execution.
+var blueslip_stacktrace = require("./blueslip_stacktrace");
+
if (Error.stackTraceLimit !== undefined) {
Error.stackTraceLimit = 100000;
}
@@ -78,20 +80,6 @@ exports.get_log = function blueslip_get_log() {
return logger.get_log();
};
-// Format error stacks using the ErrorStackParser
-// external library
-function getErrorStack(stack) {
- var ex = new Error();
- ex.stack = stack;
- return ErrorStackParser
- .parse(ex)
- .map(function (stackFrame) {
- return stackFrame.lineNumber
- + ': ' + stackFrame.fileName
- + ' | ' + stackFrame.functionName;
- }).join('\n');
-}
-
var reported_errors = {};
var last_report_attempt = {};
@@ -105,8 +93,7 @@ function report_error(msg, stack, opts) {
if (page_params.debug_mode) {
// In development, we display blueslip errors in the web UI,
// to make them hard to miss.
- stack = getErrorStack(stack);
- exports.display_errors_on_screen(msg, stack);
+ blueslip_stacktrace.display_stacktrace(msg, stack);
}
var key = ':' + msg + stack;
@@ -253,15 +240,6 @@ exports.warn = function blueslip_warn (msg, more_info) {
}
};
-exports.display_errors_on_screen = function (error, stack) {
- var $exit = "
";
- var $error = "" + error + "
";
- var $pre = "" + stack + "
";
- var $alert = $("").html($error + $exit + $pre);
-
- $(".app .alert-box").append($alert.addClass("show"));
-};
-
exports.error = function blueslip_error (msg, more_info, stack) {
if (stack === undefined) {
stack = Error().stack;
diff --git a/static/js/blueslip_stacktrace.ts b/static/js/blueslip_stacktrace.ts
new file mode 100644
index 0000000000..a196e357ee
--- /dev/null
+++ b/static/js/blueslip_stacktrace.ts
@@ -0,0 +1,97 @@
+import $ from "jquery";
+import ErrorStackParser from "error-stack-parser";
+import StackTraceGPS from "stacktrace-gps";
+import render_blueslip_stacktrace from "../templates/blueslip_stacktrace.hbs";
+
+type FunctionName = {
+ scope: string;
+ name: string;
+};
+
+type NumberedLine = {
+ line_number: number;
+ line: string;
+ focus: boolean;
+};
+
+type CleanStackFrame = {
+ full_path: string;
+ show_path: string;
+ function_name: FunctionName;
+ line_number: number;
+ context: NumberedLine[] | undefined;
+};
+
+export function clean_path(full_path: string): string {
+ // If the file is local, just show the filename.
+ // Otherwise, show the full path starting from node_modules.
+ const idx = full_path.indexOf("/node_modules/");
+ if (idx !== -1) {
+ return full_path.slice(idx + "/node_modules/".length);
+ }
+ if (full_path.startsWith("webpack://")) {
+ return full_path.slice("webpack://".length);
+ }
+ return full_path;
+}
+
+export function clean_function_name(
+ function_name: string | undefined
+): { scope: string; name: string } | undefined {
+ if (function_name === undefined) {
+ return undefined;
+ }
+ const idx = function_name.lastIndexOf(".");
+ return {
+ scope: function_name.slice(0, idx + 1),
+ name: function_name.slice(idx + 1),
+ };
+}
+
+const sourceCache: { [source: string]: string } = {};
+
+const stack_trace_gps = new StackTraceGPS({ sourceCache });
+
+function get_context(location: StackFrame.StackFrame): NumberedLine[] | undefined {
+ const sourceContent = sourceCache[location.getFileName()];
+ if (sourceContent === undefined) {
+ return undefined;
+ }
+ const lines = sourceContent.split("\n");
+ const line_number = location.getLineNumber();
+ const lo_line_num = Math.max(line_number - 5, 0);
+ const hi_line_num = Math.min(line_number + 4, lines.length);
+ return lines.slice(lo_line_num, hi_line_num).map((line: string, i: number) => ({
+ line_number: lo_line_num + i + 1,
+ line,
+ focus: lo_line_num + i + 1 === line_number,
+ }));
+}
+
+export async function display_stacktrace(error: string, stack: string): Promise {
+ const ex = new Error();
+ ex.stack = stack;
+
+ const stackframes: CleanStackFrame[] = await Promise.all(
+ ErrorStackParser.parse(ex).map(async (stack_frame: ErrorStackParser.StackFrame) => {
+ const location = await stack_trace_gps.getMappedLocation(
+ // Work around mistake in ErrorStackParser.StackFrame definition
+ // https://github.com/stacktracejs/error-stack-parser/pull/49
+ (stack_frame as unknown) as StackFrame.StackFrame
+ );
+ return {
+ full_path: location.getFileName(),
+ show_path: clean_path(location.getFileName()),
+ line_number: location.getLineNumber(),
+ function_name: clean_function_name(location.getFunctionName()),
+ context: get_context(location),
+ };
+ })
+ );
+
+ const $alert = $("").html(
+ render_blueslip_stacktrace({ error, stackframes })
+ );
+ $(".alert-box").append($alert);
+ $alert.addClass("show");
+}
diff --git a/static/js/bundles/app.js b/static/js/bundles/app.js
index b3afa027e6..0c23dbef3f 100644
--- a/static/js/bundles/app.js
+++ b/static/js/bundles/app.js
@@ -18,7 +18,6 @@ import "handlebars/dist/cjs/handlebars.runtime.js";
import "to-markdown/dist/to-markdown.js";
import "flatpickr/dist/flatpickr.js";
import "flatpickr/dist/plugins/confirmDate/confirmDate.js";
-import "error-stack-parser/dist/error-stack-parser.min.js";
import "sortablejs/Sortable.js";
import "../../generated/emoji/emoji_codes.js";
import "../../generated/pygments_data.js";
diff --git a/static/js/click_handlers.js b/static/js/click_handlers.js
index 21337a1e2b..d2228fddb0 100644
--- a/static/js/click_handlers.js
+++ b/static/js/click_handlers.js
@@ -521,8 +521,8 @@ exports.initialize = function () {
});
// this will hide the alerts that you click "x" on.
- $("body").on("click", ".alert .exit", function () {
- var $alert = $(this).closest(".alert");
+ $("body").on("click", ".alert-box > div .exit", function () {
+ var $alert = $(this).closest(".alert-box > div");
$alert.addClass("fade-out");
setTimeout(function () {
$alert.removeClass("fade-out show");
@@ -533,6 +533,10 @@ exports.initialize = function () {
settings_toggle.toggle_org_setting_collapse();
});
+ $(".alert-box").on("click", ".stackframe .expand", function () {
+ $(this).parent().siblings(".code-context").toggle("fast");
+ });
+
// COMPOSE
// NB: This just binds to current elements, and won't bind to elements
diff --git a/static/js/hbs.d.ts b/static/js/hbs.d.ts
new file mode 100644
index 0000000000..fa21b755b4
--- /dev/null
+++ b/static/js/hbs.d.ts
@@ -0,0 +1,4 @@
+declare module "*.hbs" {
+ const render: (context: unknown) => string;
+ export = render;
+}
diff --git a/static/js/tsconfig.json b/static/js/tsconfig.json
index 247b687b8c..8be0995305 100644
--- a/static/js/tsconfig.json
+++ b/static/js/tsconfig.json
@@ -1,5 +1,11 @@
{
"compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "*": ["types/*"],
+ },
+ "typeRoots": ["types"],
+
/* Typescript 3.4 added the --incremental flag but its API is not
* currently public so ts-loader cannot use it yet.
* Tracking issue: https://github.com/microsoft/TypeScript/issues/29978
@@ -8,8 +14,8 @@
/* Basic options */
"noEmit": true,
- "target": "es5",
- "module": "es6",
+ "target": "ES2019",
+ "module": "ES6",
"esModuleInterop": true,
"moduleResolution": "node",
"sourceMap": true,
@@ -25,5 +31,8 @@
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
- }
+ },
+ "exclude": [
+ "types",
+ ],
}
diff --git a/static/js/types/stacktrace-gps/index.d.ts b/static/js/types/stacktrace-gps/index.d.ts
new file mode 100644
index 0000000000..a99abc140a
--- /dev/null
+++ b/static/js/types/stacktrace-gps/index.d.ts
@@ -0,0 +1,26 @@
+// https://github.com/stacktracejs/stackframe/pull/27
+///
+
+import SourceMap from "source-map";
+
+declare namespace StackTraceGPS {
+ type StackTraceGPSOptions = {
+ sourceCache?: { [url: string]: string };
+ sourceMapConsumerCache?: { [sourceMappingUrl: string]: SourceMap.SourceMapConsumer };
+ offline?: boolean;
+ ajax?(url: string): Promise
;
+ atob?(base64: string): string;
+ };
+}
+
+// eslint-disable-next-line no-redeclare
+declare class StackTraceGPS {
+ constructor(options?: StackTraceGPS.StackTraceGPSOptions);
+ pinpoint(stackframe: StackFrame.StackFrame): Promise;
+ findFunctionName(stackframe: StackFrame.StackFrame): Promise;
+ getMappedLocation(stackframe: StackFrame.StackFrame): Promise;
+}
+
+export = StackTraceGPS;
+
+export as namespace StackTraceGPS; // global for non-module UMD users
diff --git a/static/styles/alerts.scss b/static/styles/alerts.scss
index d9a2088cde..9eee29e382 100644
--- a/static/styles/alerts.scss
+++ b/static/styles/alerts.scss
@@ -48,6 +48,104 @@ $alert-error-red: hsl(0, 80%, 40%);
width: 900px;
margin-left: calc(50% - 450px);
z-index: 220;
+ max-height: 100%;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+
+ .stacktrace {
+ @extend .alert-display;
+ }
+ .stacktrace {
+ @extend .alert-animations;
+
+ font-size: 1rem;
+ color: hsl(0, 80%, 40%);
+
+ margin-top: 5px;
+ padding: 1rem 0;
+
+ background-color: hsl(0, 100%, 98%);
+ border-radius: 4px;
+ border: 1px solid hsl(0, 80%, 40%);
+ box-shadow: 0 0 2px hsl(0, 80%, 40%);
+
+ .stacktrace-header {
+ display: flex;
+ justify-content: space-between;
+
+ .message {
+ flex: 1 1 auto;
+ }
+
+ .warning-symbol,
+ .exit {
+ flex: 0 0 auto;
+ font-size: 1.3rem;
+ padding: 0 1rem;
+ }
+
+ .exit::after {
+ cursor: pointer;
+ font-size: 2.3rem;
+ content: "\d7";
+ }
+ }
+
+ .stacktrace-content {
+ font-family: monospace;
+ font-size: 0.85rem;
+
+ margin-top: 1rem;
+
+ .stackframe {
+ padding-left: calc(3.3rem - 14px);
+ padding-right: 1rem;
+ }
+ }
+
+ .expand {
+ cursor: pointer;
+ color: hsl(0, 32%, 83%);
+
+ &:hover {
+ color: hsl(0, 0%, 20%);
+ }
+ }
+
+ .subtle {
+ color: hsl(0, 7%, 45%);
+ }
+
+ .code-context {
+ color: hsl(0, 7%, 15%);
+ background-color: hsl(0, 7%, 98%);
+ background-color: hsl(0, 7%, 98%);
+ box-shadow:
+ inset 0px 11px 10px -10px hsl(0, 7%, 70%),
+ inset 0px -11px 10px -10px hsl(0, 7%, 70%);
+
+ margin-top: 1em;
+ margin-bottom: 1em;
+
+ .code-context-content {
+ padding: 1rem 0;
+ white-space: pre;
+ overflow-x: auto;
+ }
+
+ .line-number {
+ width: 3rem;
+ display: inline-block;
+ text-align: right;
+ color: hsl(0, 7%, 35%);
+ }
+
+ .focus-line {
+ background-color: hsl(0, 7%, 90%);
+ width: 100%;
+ }
+ }
+ }
.alert {
@extend .alert-animations;
@@ -107,11 +205,6 @@ $alert-error-red: hsl(0, 80%, 40%);
}
}
}
-
- .browser-alert .error::before {
- content: "Browser Error: ";
- font-weight: 600;
- }
}
/* animation section */
diff --git a/static/styles/night_mode.scss b/static/styles/night_mode.scss
index ad4aafabad..2ba5d91973 100644
--- a/static/styles/night_mode.scss
+++ b/static/styles/night_mode.scss
@@ -462,6 +462,7 @@ on a dark background, and don't change the dark labels dark either. */
}
.alert-box .alert,
+ .alert-box .stacktrace,
.alert.alert-error {
background-color: hsl(318, 12%, 21%);
color: inherit;
@@ -472,6 +473,34 @@ on a dark background, and don't change the dark labels dark either. */
color: 1px solid hsl(0, 75%, 65%);
}
+ .alert-box .stacktrace {
+ color: hsl(314, 22%, 85%);
+
+ .expand {
+ color: hsl(318, 14%, 36%);
+ }
+
+ .subtle {
+ color: hsl(314, 19%, 63%);
+ }
+
+ .code-context {
+ color: hsl(314, 27%, 82%);
+ background-color: hsl(312, 7%, 14%);
+ box-shadow:
+ inset 0px 11px 10px -10px hsl(0, 0%, 6%),
+ inset 0px -11px 10px -10px hsl(0, 0%, 6%);
+
+ .line-number {
+ color: hsl(318, 14%, 44%);
+ }
+
+ .focus-line {
+ background-color: hsl(307, 9%, 19%);
+ }
+ }
+ }
+
/* Popover: */
.hotspot.overlay .hotspot-popover,
#hotspot_intro_reply_icon {
diff --git a/static/templates/blueslip_stacktrace.hbs b/static/templates/blueslip_stacktrace.hbs
new file mode 100644
index 0000000000..0bf81998c3
--- /dev/null
+++ b/static/templates/blueslip_stacktrace.hbs
@@ -0,0 +1,28 @@
+
+
+ {{#each stackframes}}
+
+
+
+ at
+ {{#if function_name}}
+ {{ function_name.scope }}{{ function_name.name }}
+ {{/if}}
+ {{ show_path }}:{{ line_number }}
+
+
+
+ {{~#each context~}}
+
{{ line_number }} {{ line }}
+ {{~/each~}}
+
+
+
+ {{/each}}
+
diff --git a/version.py b/version.py
index 92358662ec..a3ee84d78a 100644
--- a/version.py
+++ b/version.py
@@ -26,4 +26,4 @@ LATEST_RELEASE_ANNOUNCEMENT = "https://blog.zulip.org/2019/03/01/zulip-2-0-relea
# historical commits sharing the same major version, in which case a
# minor version bump suffices.
-PROVISION_VERSION = '62.0'
+PROVISION_VERSION = '62.1'
diff --git a/yarn.lock b/yarn.lock
index 16b13dd85c..484f7a9c4f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -915,6 +915,13 @@
dependencies:
"@types/node" "*"
+"@types/jquery@^3.3.31":
+ version "3.3.31"
+ resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b"
+ integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==
+ dependencies:
+ "@types/sizzle" "*"
+
"@types/json-schema@^7.0.3":
version "7.0.3"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
@@ -953,6 +960,11 @@
"@types/express-serve-static-core" "*"
"@types/mime" "*"
+"@types/sizzle@*":
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
+ integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==
+
"@types/source-list-map@*":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
@@ -9353,7 +9365,7 @@ regenerate@^1.4.0:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
-regenerator-runtime@^0.13.2:
+regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3:
version "0.13.3"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==
@@ -10323,6 +10335,11 @@ source-map-url@^0.4.0:
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+source-map@0.5.6:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
+ integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
+
"source-map@>= 0.1.2":
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
@@ -10495,6 +10512,14 @@ stackframe@^1.1.0:
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.1.0.tgz#e3fc2eb912259479c9822f7d1f1ff365bd5cbc83"
integrity sha512-Vx6W1Yvy+AM1R/ckVwcHQHV147pTPBKWCRLrXMuPrFVfvBUc3os7PR1QLIWCMhPpRg5eX9ojzbQIMLGBwyLjqg==
+stacktrace-gps@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.3.tgz#b89f84cc13bb925b96607e737b617c8715facf57"
+ integrity sha512-51Rr7dXkyFUKNmhY/vqZWK+EvdsfFSRiQVtgHTFlAdNIYaDD7bVh21yBHXaNWAvTD+w+QSjxHg7/v6Tz4veExA==
+ dependencies:
+ source-map "0.5.6"
+ stackframe "^1.1.0"
+
state-toggle@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.2.tgz#75e93a61944116b4959d665c8db2d243631d6ddc"