Skip to content

Commit cbd8793

Browse files
authored
Merge pull request #4483 from easyops-cn/steve/v3-error-handling
Steve/v3-error-handling
2 parents 4c48eea + d9fa28e commit cbd8793

File tree

15 files changed

+270
-69
lines changed

15 files changed

+270
-69
lines changed

packages/brick-container/src/BootstrapError.shadow.css renamed to packages/brick-container/src/DefaultError.shadow.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@
3131
line-height: 1.6;
3232
text-align: center;
3333
}
34+
35+
.actions {
36+
margin-top: 1em;
37+
}
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// istanbul ignore file
2-
import styleText from "./BootstrapError.shadow.css";
2+
import { escape } from "lodash";
3+
import styleText from "./DefaultError.shadow.css";
34

45
const icon = `<svg viewBox="64 64 896 896" focusable="false" data-icon="close-circle" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"></path></svg>`;
56

6-
export class BootstrapError extends HTMLElement {
7+
export class DefaultError extends HTMLElement {
8+
errorTitle: string | undefined;
9+
710
connectedCallback() {
811
if (this.shadowRoot) {
912
return;
@@ -12,8 +15,9 @@ export class BootstrapError extends HTMLElement {
1215
shadowRoot.innerHTML = [
1316
`<style>${styleText}</style>`,
1417
`<div class="icon">${icon}</div>`,
15-
'<div class="title">启动错误</div>',
18+
`<div class="title">${escape(this.errorTitle)}</div>`,
1619
'<div class="description"><slot></slot></div>',
20+
`<div class="actions"><slot name="link"></slot></div>`,
1721
].join("");
1822
}
1923
}

packages/brick-container/src/bootstrap.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ import { imagesFactory, widgetImagesFactory } from "./images.js";
2424
import { getSpanId } from "./utils.js";
2525
import { listen } from "./preview/listen.js";
2626
import { getMock } from "./mocks.js";
27-
import { NS, locales } from "./i18n.js";
28-
import { BootstrapError } from "./BootstrapError.js";
27+
import { NS, K, locales } from "./i18n.js";
28+
import { DefaultError } from "./DefaultError.js";
29+
30+
customElements.define("easyops-default-error", DefaultError);
2931

3032
analytics.initialize(
3133
`${getBasePath()}api/gateway/data_exchange.store.ClickHouseInsertData/api/v1/data_exchange/frontend_stat`
@@ -141,9 +143,16 @@ async function main() {
141143
// `.bootstrap-error` makes loading-bar invisible.
142144
document.body.classList.add("bootstrap-error");
143145

144-
customElements.define("easyops-bootstrap-error", BootstrapError);
145-
const errorElement = document.createElement("easyops-bootstrap-error");
146+
const errorElement = document.createElement(
147+
"easyops-default-error"
148+
) as DefaultError;
149+
errorElement.errorTitle = i18n.t(`${NS}:${K.BOOTSTRAP_ERROR}`);
146150
errorElement.textContent = httpErrorToString(error);
151+
const linkElement = document.createElement("a");
152+
linkElement.slot = "link";
153+
linkElement.href = location.href;
154+
linkElement.textContent = i18n.t(`${NS}:${K.RELOAD}`);
155+
errorElement.appendChild(linkElement);
147156

148157
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
149158
document.querySelector("#main-mount-point")!.replaceChildren(errorElement);

packages/brick-container/src/i18n.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
export enum K {
22
LOGIN_CHANGED = "LOGIN_CHANGED",
33
LOGOUT_APPLIED = "LOGOUT_APPLIED",
4+
BOOTSTRAP_ERROR = "BOOTSTRAP_ERROR",
5+
RELOAD = "RELOAD",
46
}
57

68
const en: Locale = {
79
[K.LOGIN_CHANGED]:
810
"You have logged in as another account, click OK to refresh the page.",
911
[K.LOGOUT_APPLIED]:
1012
"Your account has been logged out, click OK to refresh the page.",
13+
[K.BOOTSTRAP_ERROR]: "Bootstrap Error",
14+
[K.RELOAD]: "Reload",
1115
};
1216

1317
const zh: Locale = {
1418
[K.LOGIN_CHANGED]: "您已经登录另一个账号,点击确定刷新页面。",
1519
[K.LOGOUT_APPLIED]: "您的账号已经登出,点击确定刷新页面。",
20+
[K.BOOTSTRAP_ERROR]: "启动错误",
21+
[K.RELOAD]: "刷新",
1622
};
1723

1824
export const NS = "-/container";

packages/loader/src/stableLoadBricks.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, jest, test, expect } from "@jest/globals";
2-
import {
2+
import type {
33
loadBricksImperatively as _loadBricksImperatively,
44
loadProcessorsImperatively as _loadProcessorsImperatively,
55
loadEditorsImperatively as _loadEditorsImperatively,

packages/loader/src/stableLoadBricks.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ interface BrickPackage {
1515
}
1616

1717
let resolveBasicPkg: () => void;
18+
let rejectBasicPkg: (e: unknown) => void;
1819
let basicPkgWillBeResolved = false;
19-
const waitBasicPkg = new Promise<void>((resolve) => {
20+
const waitBasicPkg = new Promise<void>((resolve, reject) => {
2021
resolveBasicPkg = resolve;
22+
rejectBasicPkg = reject;
2123
});
2224

2325
export function flushStableLoadBricks(): void {
@@ -264,7 +266,9 @@ async function enqueueStableLoad(
264266
// Packages other than BASIC will wait for an extra micro-task tick.
265267
if (!basicPkgWillBeResolved) {
266268
basicPkgWillBeResolved = true;
267-
tempPromise.then(() => Promise.resolve()).then(resolveBasicPkg);
269+
tempPromise
270+
.then(() => Promise.resolve())
271+
.then(resolveBasicPkg, rejectBasicPkg);
268272
}
269273
basicPkgPromise = tempPromise.then(() =>
270274
Promise.all(
@@ -310,7 +314,7 @@ async function enqueueStableLoad(
310314
return adapter.resolve(
311315
v2Adapter.filePath,
312316
type === "editors"
313-
? pkg.propertyEditorsJsFilePath ?? pkg.filePath
317+
? (pkg.propertyEditorsJsFilePath ?? pkg.filePath)
314318
: pkg.filePath,
315319
type === "bricks"
316320
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion

packages/runtime/src/createRoot.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ describe("preview", () => {
240240
]);
241241

242242
expect(container.innerHTML).toBe(
243-
'<div data-error-boundary="">UNKNOWN_ERROR: ReferenceError: QUERY is not defined, in "&lt;% QUERY.q %&gt;"</div>'
243+
'<div data-error-boundary=""><div>UNKNOWN_ERROR: ReferenceError: QUERY is not defined, in "&lt;% QUERY.q %&gt;"</div></div>'
244244
);
245245
expect(portal.innerHTML).toBe("");
246246
expect(applyTheme).not.toBeCalled();

packages/runtime/src/internal/ErrorNode.spec.ts

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,26 @@ describe("ErrorNode", () => {
4242
} as RenderReturnNode)
4343
).toEqual({
4444
properties: {
45+
errorTitle: "UNKNOWN_ERROR",
4546
dataset: {
4647
errorBoundary: "",
4748
},
4849
style: {
4950
color: "var(--color-error)",
5051
},
51-
textContent: "UNKNOWN_ERROR: Error: oops",
5252
},
5353
return: {
5454
tag: 1,
5555
},
5656
runtimeContext: null,
5757
tag: 2,
5858
type: "div",
59+
child: expect.objectContaining({
60+
type: "div",
61+
properties: {
62+
textContent: "UNKNOWN_ERROR: Error: oops",
63+
},
64+
}),
5965
});
6066
});
6167

@@ -72,20 +78,26 @@ describe("ErrorNode", () => {
7278
)
7379
).toEqual({
7480
properties: {
81+
errorTitle: "NO_PERMISSION",
7582
dataset: {
7683
errorBoundary: "",
7784
},
7885
style: {
7986
color: "var(--color-error)",
8087
},
81-
textContent: "NO_PERMISSION: HttpResponseError: Forbidden",
8288
},
8389
return: {
8490
tag: 1,
8591
},
8692
runtimeContext: null,
8793
tag: 2,
8894
type: "div",
95+
child: expect.objectContaining({
96+
type: "div",
97+
properties: {
98+
textContent: "NO_PERMISSION: HttpResponseError: Forbidden",
99+
},
100+
}),
89101
});
90102
});
91103

@@ -96,20 +108,26 @@ describe("ErrorNode", () => {
96108
} as RenderReturnNode)
97109
).toEqual({
98110
properties: {
111+
errorTitle: "NETWORK_ERROR",
99112
dataset: {
100113
errorBoundary: "",
101114
},
102115
style: {
103116
color: "var(--color-error)",
104117
},
105-
textContent: "NETWORK_ERROR: BrickLoadError: oops",
106118
},
107119
return: {
108120
tag: 1,
109121
},
110122
runtimeContext: null,
111123
tag: 2,
112124
type: "div",
125+
child: expect.objectContaining({
126+
type: "div",
127+
properties: {
128+
textContent: "NETWORK_ERROR: BrickLoadError: oops",
129+
},
130+
}),
113131
});
114132
});
115133

@@ -214,20 +232,26 @@ describe("ErrorNode", () => {
214232

215233
expect(await promise).toEqual({
216234
properties: {
235+
errorTitle: "LICENSE_EXPIRED",
217236
dataset: {
218237
errorBoundary: "",
219238
},
220239
style: {
221240
color: "var(--color-error)",
222241
},
223-
textContent: "LICENSE_EXPIRED: HttpResponseError: Bad Request",
224242
},
225243
return: {
226244
tag: 1,
227245
},
228246
runtimeContext: null,
229247
tag: 2,
230248
type: "div",
249+
child: expect.objectContaining({
250+
type: "div",
251+
properties: {
252+
textContent: "LICENSE_EXPIRED: HttpResponseError: Bad Request",
253+
},
254+
}),
231255
});
232256

233257
expect(consoleError).toBeCalledTimes(1);
@@ -263,7 +287,7 @@ describe("ErrorNode", () => {
263287
type: "eo-link",
264288
properties: {
265289
textContent: "GO_BACK_HOME",
266-
url: "/",
290+
href: "/",
267291
},
268292
events: undefined,
269293
}),
@@ -298,7 +322,7 @@ describe("ErrorNode", () => {
298322
type: "eo-link",
299323
properties: {
300324
textContent: "GO_BACK_HOME",
301-
url: "/",
325+
href: "/",
302326
},
303327
events: undefined,
304328
}),
@@ -320,20 +344,85 @@ describe("ErrorNode", () => {
320344
)
321345
).toEqual({
322346
properties: {
347+
errorTitle: "APP_NOT_FOUND",
323348
dataset: {
324349
errorBoundary: "",
325350
},
326351
style: {
327352
color: "var(--color-error)",
328353
},
329-
textContent: "APP_NOT_FOUND",
330354
},
331355
return: {
332356
tag: 1,
333357
},
334358
runtimeContext: null,
335359
tag: 2,
336360
type: "div",
361+
child: expect.objectContaining({
362+
type: "div",
363+
properties: {
364+
textContent: "APP_NOT_FOUND",
365+
},
366+
}),
337367
});
338368
});
339369
});
370+
371+
describe("with default error brick", () => {
372+
beforeAll(() => {
373+
customElements.define(
374+
"easyops-default-error",
375+
class extends HTMLElement {}
376+
);
377+
});
378+
379+
test("page level with script error and default error brick", async () => {
380+
const consoleError = jest.spyOn(console, "error").mockReturnValue();
381+
const script = document.createElement("script");
382+
script.src = "fail.js";
383+
const scriptError = new Event("error");
384+
Object.defineProperty(scriptError, "target", { value: script });
385+
mockedLoadBricks.mockRejectedValueOnce(new Error("oops"));
386+
387+
expect(
388+
await ErrorNode(
389+
scriptError,
390+
{
391+
tag: RenderTag.ROOT,
392+
} as RenderReturnNode,
393+
true
394+
)
395+
).toEqual({
396+
properties: {
397+
errorTitle: "NETWORK_ERROR",
398+
dataset: {
399+
errorBoundary: "",
400+
},
401+
style: {
402+
color: "var(--color-error)",
403+
},
404+
},
405+
return: {
406+
tag: 1,
407+
},
408+
runtimeContext: null,
409+
tag: 2,
410+
type: "easyops-default-error",
411+
child: expect.objectContaining({
412+
type: "div",
413+
properties: {
414+
textContent: "http://localhost/fail.js",
415+
},
416+
sibling: expect.objectContaining({
417+
type: "a",
418+
slotId: "link",
419+
properties: {
420+
textContent: "RELOAD",
421+
href: "http://localhost/",
422+
},
423+
}),
424+
}),
425+
});
426+
consoleError.mockRestore();
427+
});
428+
});

0 commit comments

Comments
 (0)