Improve frontend error messages written to system log (#17616)

This commit is contained in:
Steve Repsher 2023-08-21 07:01:42 -04:00 committed by GitHub
parent bf912f7bd3
commit 46a036ddbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 200 additions and 37 deletions

View File

@ -202,7 +202,10 @@ const createWebpackConfig = ({
!existsSync(info.resourcePath) ||
info.resourcePath.startsWith("./node_modules")
) {
return new URL(info.resourcePath, "webpack://frontend/").href;
// Source URLs are unknown for dependencies, so we use a relative URL with a
// non - existent top directory. This results in a clean source tree in browser
// dev tools, and they stay happy getting 404s with valid requests.
return `/unknown${path.resolve("/", info.resourcePath)}`;
}
return new URL(info.resourcePath, bundle.sourceMapURL()).href;
}

View File

@ -133,10 +133,12 @@
"roboto-fontface": "0.10.0",
"rrule": "2.7.2",
"sortablejs": "1.15.0",
"stacktrace-js": "2.0.2",
"superstruct": "1.0.3",
"tinykeys": "2.1.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "1.0.35",
"unfetch": "5.0.0",
"vis-data": "7.1.6",
"vis-network": "9.1.6",
@ -182,6 +184,7 @@
"@types/serve-handler": "6.1.1",
"@types/sortablejs": "1.15.1",
"@types/tar": "6.1.5",
"@types/ua-parser-js": "0.7.36",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.4.0",
"@typescript-eslint/parser": "6.4.0",

View File

@ -1,15 +1,21 @@
import { HomeAssistant, TranslationDict } from "../types";
import { HomeAssistant } from "../types";
export type SystemLogLevel =
| "critical"
| "error"
| "warning"
| "info"
| "debug";
export interface LoggedError {
name: string;
message: [string];
level: keyof TranslationDict["ui"]["panel"]["config"]["logs"]["level"];
level: SystemLogLevel;
source: [string, number];
// unix timestamp in seconds
timestamp: number;
exception: string;
count: number;
// unix timestamp in seconds
// unix timestamps in seconds
timestamp: number;
first_occurred: number;
}

View File

@ -25,7 +25,6 @@ import { subscribeUser } from "../data/ws-user";
import type { ExternalAuth } from "../external_app/external_auth";
import "../resources/array.flat.polyfill";
import "../resources/safari-14-attachshadow-patch";
import { HomeAssistant } from "../types";
import { MAIN_WINDOW_NAME } from "../data/main_window";
window.name = MAIN_WINDOW_NAME;
@ -132,32 +131,3 @@ window.hassConnection.then(({ conn }) => {
llWindow.llResProm = fetchResources(conn);
}
});
window.addEventListener("error", (e) => {
if (
!__DEV__ &&
typeof e.message === "string" &&
(e.message.includes("ResizeObserver loop limit exceeded") ||
e.message.includes(
"ResizeObserver loop completed with undelivered notifications"
))
) {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
return;
}
const homeAssistant = document.querySelector("home-assistant") as any;
if (
homeAssistant &&
homeAssistant.hass &&
(homeAssistant.hass as HomeAssistant).callService
) {
homeAssistant.hass.callService("system_log", "write", {
logger: `frontend.${
__DEV__ ? "js_dev" : "js"
}.${__BUILD__}.${__VERSION__.replace(".", "")}`,
message: `${e.filename}:${e.lineno}:${e.colno} ${e.message}`,
});
}
});

View File

@ -0,0 +1,72 @@
import "core-js/modules/web.url.can-parse";
import { fromError } from "stacktrace-js";
import { UAParser } from "ua-parser-js";
// URL paths to remove from filenames and max stack trace lines for brevity
const REMOVAL_PATHS =
/^\/(?:home-assistant\/frontend\/[^/]+|unknown|\/{2}\.)\//;
const MAX_STACK_FRAMES = 10;
export const createLogMessage = async (
error: unknown,
intro?: string,
messageFallback?: string,
stackFallback?: string
) => {
const lines: (string | undefined)[] = [];
// Append the originating browser/OS to any intro for easier identification
if (intro) {
const parser = new UAParser();
const {
name: browserName = "unknown browser",
version: browserVersion = "",
} = parser.getBrowser();
const { name: osName = "unknown OS", version: osVersion = "" } =
parser.getOS();
const browser = `${browserName} ${browserVersion}`.trim();
const os = `${osName} ${osVersion}`.trim();
lines.push(`${intro} from ${browser} on ${os}`);
}
// In most cases, an Error instance will be thrown, which can have many details to log:
// - a standard string coercion to "ErrorType: Message"
// - a stack added by browsers (which must be converted to original source)
// - an optional cause chain
// - a possible list of aggregated errors
if (error instanceof Error) {
lines.push(error.toString() || messageFallback);
const stackLines = (await fromError(error))
.slice(0, MAX_STACK_FRAMES)
.map((frame) => {
frame.fileName ??= "";
// @ts-expect-error canParse not in DOM library yet
if (URL.canParse(frame.fileName)) {
frame.fileName = new URL(frame.fileName).pathname;
}
frame.fileName = frame.fileName.replace(REMOVAL_PATHS, "");
return frame.toString();
});
lines.push(...(stackLines.length > 0 ? stackLines : [stackFallback]));
// @ts-expect-error Requires library bump to ES2022
if (error.cause) {
// @ts-expect-error Requires library bump to ES2022
lines.push(`Caused by: ${await createLogMessage(error.cause)}`);
}
if (error instanceof AggregateError) {
const subMessageEntries = error.errors.map(
async (e, i) => [i, await createLogMessage(e)] as const
);
for await (const [i, m] of subMessageEntries) {
lines.push(`Part ${i + 1} of ${error.errors.length}: ${m}`);
}
}
} else {
// The error could be anything, so just stringify it and log with fallbacks
const errorString = JSON.stringify(error, null, 2);
lines.push(
messageFallback,
errorString === messageFallback ? "" : errorString,
stackFallback
);
}
return lines.filter(Boolean).join("\n");
};

View File

@ -1,9 +1,10 @@
import { HASSDomEvent } from "../common/dom/fire_event";
import { SystemLogLevel } from "../data/system_log";
import { Constructor } from "../types";
import { HassBaseEl } from "./hass-base-mixin";
interface WriteLogParams {
level?: "debug" | "info" | "warning" | "error" | "critical";
level?: SystemLogLevel;
message: string;
}
@ -21,6 +22,44 @@ export const loggingMixin = <T extends Constructor<HassBaseEl>>(
superClass: T
) =>
class extends superClass {
protected hassConnected() {
super.hassConnected();
window.addEventListener("error", async (ev) => {
if (
!__DEV__ &&
(ev.message.includes("ResizeObserver loop limit exceeded") ||
ev.message.includes(
"ResizeObserver loop completed with undelivered notifications"
))
) {
ev.preventDefault();
ev.stopImmediatePropagation();
ev.stopPropagation();
return;
}
const { createLogMessage } = await import("../resources/log-message");
this._writeLog({
// The error object from browsers includes the message and a stack trace,
// so use the data in the error event just as fallback
message: await createLogMessage(
ev.error,
"Uncaught error",
ev.message,
`@${ev.filename}:${ev.lineno}:${ev.colno}`
),
});
});
window.addEventListener("unhandledrejection", async (ev) => {
const { createLogMessage } = await import("../resources/log-message");
this._writeLog({
message: await createLogMessage(
ev.reason,
"Unhandled promise rejection"
),
});
});
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.addEventListener("write_log", (ev) => {

View File

@ -4602,6 +4602,13 @@ __metadata:
languageName: node
linkType: hard
"@types/ua-parser-js@npm:0.7.36":
version: 0.7.36
resolution: "@types/ua-parser-js@npm:0.7.36"
checksum: 8c24d4dc12ed1b8b98195838093391c358c81bf75e9cae0ecec8f7824b441e069daaa17b974a3e257172caddb671439f0c0b44bf43bfcf409b7a574a25aab948
languageName: node
linkType: hard
"@types/webspeechapi@npm:0.0.29":
version: 0.0.29
resolution: "@types/webspeechapi@npm:0.0.29"
@ -7753,6 +7760,15 @@ __metadata:
languageName: node
linkType: hard
"error-stack-parser@npm:^2.0.6":
version: 2.1.4
resolution: "error-stack-parser@npm:2.1.4"
dependencies:
stackframe: ^1.3.4
checksum: 3b916d2d14c6682f287c8bfa28e14672f47eafe832701080e420e7cdbaebb2c50293868256a95706ac2330fe078cf5664713158b49bc30d7a5f2ac229ded0e18
languageName: node
linkType: hard
"es-abstract@npm:^1.19.0, es-abstract@npm:^1.20.4, es-abstract@npm:^1.21.2":
version: 1.22.1
resolution: "es-abstract@npm:1.22.1"
@ -9700,6 +9716,7 @@ __metadata:
"@types/serve-handler": 6.1.1
"@types/sortablejs": 1.15.1
"@types/tar": 6.1.5
"@types/ua-parser-js": 0.7.36
"@types/webspeechapi": 0.0.29
"@typescript-eslint/eslint-plugin": 6.4.0
"@typescript-eslint/parser": 6.4.0
@ -9790,6 +9807,7 @@ __metadata:
sinon: 15.2.0
sortablejs: 1.15.0
source-map-url: 0.4.1
stacktrace-js: 2.0.2
superstruct: 1.0.3
systemjs: 6.14.2
tar: 6.1.15
@ -9799,6 +9817,7 @@ __metadata:
tsparticles-engine: 2.12.0
tsparticles-preset-links: 2.12.0
typescript: 5.1.6
ua-parser-js: 1.0.35
unfetch: 5.0.0
vinyl-buffer: 1.0.1
vinyl-source-stream: 2.0.0
@ -14500,6 +14519,13 @@ __metadata:
languageName: node
linkType: hard
"source-map@npm:0.5.6":
version: 0.5.6
resolution: "source-map@npm:0.5.6"
checksum: 390b3f5165c9631a74fb6fb55ba61e62a7f9b7d4026ae0e2bfc2899c241d71c1bccb8731c496dc7f7cb79a5f523406eb03d8c5bebe8448ee3fc38168e2d209c8
languageName: node
linkType: hard
"source-map@npm:^0.5.6":
version: 0.5.7
resolution: "source-map@npm:0.5.7"
@ -14623,6 +14649,15 @@ __metadata:
languageName: node
linkType: hard
"stack-generator@npm:^2.0.5":
version: 2.0.10
resolution: "stack-generator@npm:2.0.10"
dependencies:
stackframe: ^1.3.4
checksum: 4fc3978a934424218a0aa9f398034e1f78153d5ff4f4ff9c62478c672debb47dd58de05b09fc3900530cbb526d72c93a6e6c9353bacc698e3b1c00ca3dda0c47
languageName: node
linkType: hard
"stack-trace@npm:0.0.10":
version: 0.0.10
resolution: "stack-trace@npm:0.0.10"
@ -14630,6 +14665,34 @@ __metadata:
languageName: node
linkType: hard
"stackframe@npm:^1.3.4":
version: 1.3.4
resolution: "stackframe@npm:1.3.4"
checksum: bae1596873595c4610993fa84f86a3387d67586401c1816ea048c0196800c0646c4d2da98c2ee80557fd9eff05877efe33b91ba6cd052658ed96ddc85d19067d
languageName: node
linkType: hard
"stacktrace-gps@npm:^3.0.4":
version: 3.1.2
resolution: "stacktrace-gps@npm:3.1.2"
dependencies:
source-map: 0.5.6
stackframe: ^1.3.4
checksum: 85daa232d138239b6ae0f4bcdd87d15d302a045d93625db17614030945b5314e204b5fbcf9bee5b6f4f9e6af5fca05f65c27fe910894b861ef6853b99470aa1c
languageName: node
linkType: hard
"stacktrace-js@npm:2.0.2":
version: 2.0.2
resolution: "stacktrace-js@npm:2.0.2"
dependencies:
error-stack-parser: ^2.0.6
stack-generator: ^2.0.5
stacktrace-gps: ^3.0.4
checksum: 081e786d56188ac04ac6604c09cd863b3ca2b4300ec061366cf68c3e4ad9edaa34fb40deea03cc23a05f442aa341e9171f47313f19bd588f9bec6c505a396286
languageName: node
linkType: hard
"static-extend@npm:^0.1.1":
version: 0.1.2
resolution: "static-extend@npm:0.1.2"
@ -15581,6 +15644,13 @@ __metadata:
languageName: node
linkType: hard
"ua-parser-js@npm:1.0.35":
version: 1.0.35
resolution: "ua-parser-js@npm:1.0.35"
checksum: 02370d38a0c8b586f2503d1c3bbba5cbc0b97d407282f9023201a99e4c03eae4357a2800fdf50cf80d73ec25c0b0cc5bfbaa03975b0add4043d6e4c86712c9c1
languageName: node
linkType: hard
"unbox-primitive@npm:^1.0.2":
version: 1.0.2
resolution: "unbox-primitive@npm:1.0.2"