blueslip: Make stack trace more readable.

The stack trace popup is now sourcemapped and each stackframe have a
expandable code context window.

[anders@zulipchat.com: Rebased and simplified.]
This commit is contained in:
Thomas Ip 2019-06-20 17:59:55 +08:00 committed by Tim Abbott
parent 94c8fffdf3
commit c93522d847
15 changed files with 391 additions and 39 deletions

View File

@ -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",

View File

@ -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",
}
);
});

View File

@ -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",

View File

@ -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 = "<div class='exit'></div>";
var $error = "<div class='error'>" + error + "</div>";
var $pre = "<pre>" + stack + "</pre>";
var $alert = $("<div class='alert browser-alert home-error-bar'></div>").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;

View File

@ -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<void> {
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 = $("<div class='stacktrace'>").html(
render_blueslip_stacktrace({ error, stackframes })
);
$(".alert-box").append($alert);
$alert.addClass("show");
}

View File

@ -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";

View File

@ -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

4
static/js/hbs.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "*.hbs" {
const render: (context: unknown) => string;
export = render;
}

View File

@ -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",
],
}

View File

@ -0,0 +1,26 @@
// https://github.com/stacktracejs/stackframe/pull/27
/// <reference types="stackframe/stackframe" />
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<string>;
atob?(base64: string): string;
};
}
// eslint-disable-next-line no-redeclare
declare class StackTraceGPS {
constructor(options?: StackTraceGPS.StackTraceGPSOptions);
pinpoint(stackframe: StackFrame.StackFrame): Promise<StackFrame.StackFrame>;
findFunctionName(stackframe: StackFrame.StackFrame): Promise<StackFrame.StackFrame>;
getMappedLocation(stackframe: StackFrame.StackFrame): Promise<StackFrame.StackFrame>;
}
export = StackTraceGPS;
export as namespace StackTraceGPS; // global for non-module UMD users

View File

@ -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 */

View File

@ -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 {

View File

@ -0,0 +1,28 @@
<div class="stacktrace-header">
<div class="warning-symbol">
<i class="fa fa-exclamation-triangle"></i>
</div>
<div class="message"><strong>Error:</strong> {{ error }}</div>
<div class="exit"></div>
</div>
<div class="stacktrace-content">
{{#each stackframes}}
<div data-full-path="{{ full_path }}" data-line-no="{{ line_number }}">
<div class="stackframe">
<i class="fa fa-caret-right expand"></i>
<span class="subtle">at</span>
{{#if function_name}}
{{ function_name.scope }}<b>{{ function_name.name }}</b>
{{/if}}
<span class="subtle">{{ show_path }}:{{ line_number }}</span>
</div>
<div class="code-context" style="display: none">
<div class="code-context-content">
{{~#each context~}}
<div {{#if focus}}class="focus-line"{{/if}}><span class="line-number">{{ line_number }}</span> {{ line }}</div>
{{~/each~}}
</div>
</div>
</div>
{{/each}}
</div>

View File

@ -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'

View File

@ -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"