mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-10 10:56:34 +00:00
Improve frontend error messages written to system log (#17616)
This commit is contained in:
parent
bf912f7bd3
commit
46a036ddbe
@ -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;
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
72
src/resources/log-message.ts
Normal file
72
src/resources/log-message.ts
Normal 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");
|
||||
};
|
@ -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) => {
|
||||
|
70
yarn.lock
70
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user