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 @@ +
+
+ +
+
Error: {{ error }}
+
+
+
+ {{#each stackframes}} +
+
+ + at + {{#if function_name}} + {{ function_name.scope }}{{ function_name.name }} + {{/if}} + {{ show_path }}:{{ line_number }} +
+ +
+ {{/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"