-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathglobal-errors.ts
More file actions
143 lines (127 loc) · 6.35 KB
/
Copy pathglobal-errors.ts
File metadata and controls
143 lines (127 loc) · 6.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import {AppNotification} from '$lib/notifications/notifications';
import type {IJsInvokableLogger} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/IJsInvokableLogger';
import {LogLevel} from '$lib/dotnet-types/generated-types/Microsoft/Extensions/Logging/LogLevel';
import {delay} from '$lib/utils/time';
import {useJsInvokableLogger} from '$lib/services/js-invokable-logger';
type UnifiedErrorEvent = {
message: string;
error: unknown;
at?: string;
}
// message is normalized here to a display-ready title so consumers don't each re-derive one. For any Error we
// use toString() ("TypeError: x", not the bare "x") to keep the type, and drop the browser's "Uncaught" prefix.
export function unifyErrorEvent(event: ErrorEvent | PromiseRejectionEvent | Error): UnifiedErrorEvent {
if (event instanceof Error) {
return { message: event.toString(), error: event };
} else if ('message' in event) {
return { message: event.error instanceof Error ? event.error.toString() : event.message, error: event.error, at: `${event.filename}:${event.lineno}:${event.colno}` };
} else if (typeof event.reason === 'string') {
return { message: event.reason, error: null };
} else if (event.reason instanceof Error) {
return { message: event.reason.toString(), error: event.reason };
} else {
return { message: 'Unknown error', error: event.reason };
}
}
function shouldIgnoreError(message: string): boolean {
if (message.includes('Perhaps the DotNetObjectReference instance was already disposed')) return true;
// Blazor WebView completing a JS->.NET call whose JS-side registry was already torn down (e.g. page refresh).
if (message.includes('There is no pending async call with ID')) return true;
// Code (i.e. {expression}) inside a <MenuItem> slot, inside a portal causes this error if the portal is open while the screen is resized 🙃
// It's worth noting that in Lexbox we've also seen browser extensions trigger this error
if (message.includes('ResizeObserver loop completed with undelivered notifications')) return true;
// Harmless error that seems to be caused by animate-out css inside a portal that is removed from the DOM.
// Tried hard to make a repro for bits-ui, but failed. Occurs whenever a dropdown (e.g.) is open during navigation or similar.
if (message.includes('this.opts.onOpenChangeComplete.current')) return true;
return false;
}
/** Splits a .NET error string into its leading message and the rest (stack/inner exceptions), at whichever
comes first: the first stack frame (" at ") or the first inner-exception marker (" ---> "). The latter matters
because .NET prints the whole inner-exception chain before the outer frames, so without it a deeply-wrapped
error (e.g. MSAL wrapping an Android network failure) dumps the entire cascade into the title.
System.InvalidOperationException: Everything is broken. Here's some ice cream.
at FwLiteShared.Services.ProjectServicesProvider.OpenCrdtProject(String projectName)
*/
const dotnetErrorRegex = /^([\s\S]+?)(?: {3}at | ---> )/m;
export function processErrorIntoDetails(event: UnifiedErrorEvent): {message: string, detail?: string} {
const message = event.message;
const match = dotnetErrorRegex.exec(message);
if (match) return {message: match[1].trim(), detail: message.substring(match[1].length).trim()};
// stackFrames drops the leading "Error: <message>" header so the message isn't shown in both slots.
else if (event.error instanceof Error) return {message, detail: stackFrames(event.error)};
else return {message};
}
function stackFrames(error: Error): string | undefined {
const stack = error.stack;
if (!stack) return undefined;
// the stack seems to sometimes start with the error message, so we drop it to avoid duplication in the UI
const header = error.toString();
if (stack.startsWith(header)) return stack.slice(header.length).trim();
// perhaps redundant, but cheap
else if (stack.startsWith(error.message)) return stack.slice(error.message.length).trim();
return stack;
}
let setup = false;
export function setupGlobalErrorHandlers() {
if (setup) return;
setup = true;
window.addEventListener('error', onErrorEvent);
window.addEventListener('unhandledrejection', onErrorEvent);
}
export function onErrorEvent(event: ErrorEvent | PromiseRejectionEvent | Error) {
const errorEvent = unifyErrorEvent(event);
if (shouldIgnoreError(errorEvent.message)) return;
void tryLogErrorToDotNet(errorEvent);
const {message: simpleMessage, detail} = processErrorIntoDetails(errorEvent);
AppNotification.error(simpleMessage, detail);
}
async function tryLogErrorToDotNet(error: UnifiedErrorEvent) {
try {
const details = getErrorString(error);
if (!safeToLogErrorToDotNet(details)) return;
const logger = await tryGetLogger();
if (logger) await logger.log(LogLevel.Error, details);
else console.warn('No DotNet logger available to log error', error);
} catch (err) {
console.error('Failed to log error to DotNet', err);
}
}
function safeToLogErrorToDotNet(details: string): boolean {
// likely cyclical errors
if (details.includes('JsInvokableLogger')) return false;
if (details.includes('tryLogErrorToDotNet')) return false;
// dotnet is not available (can also be cyclical)
if (details.includes('Cannot send data if the connection is not in the \'Connected\' State')) return false;
return true;
}
// some very cheap durability.
// As it is today, the logger service is available before our error handlers are registered
async function tryGetLogger(): Promise<IJsInvokableLogger | undefined> {
let logger = useJsInvokableLogger();
if (logger) return logger;
await delay(1);
logger = useJsInvokableLogger();
if (logger) return logger;
await delay(1000);
logger = useJsInvokableLogger();
return logger;
}
function getErrorString(event: UnifiedErrorEvent) {
const details = [`Message: ${event.message}`];
if (event.at) details.push(`at ${event.at}`);
if (event.error instanceof Error) {
const error: Error = event.error;
if (error.stack) details.push(`Stack: ${error.stack}`);
if (error.cause) details.push(`Cause: ${tryStringify(error.cause)}`);
} else if (event.error) {
details.push(`Error: ${tryStringify(event.error)}`);
}
return details.join('\n');
}
function tryStringify(value: unknown): string | undefined {
try {
return JSON.stringify(value);
} catch {
return '(failed-to-stringify)';
}
}