Skip to content

Commit fc03203

Browse files
feat/handling exceptions from browser window events
1 parent 7a84046 commit fc03203

6 files changed

Lines changed: 158 additions & 16 deletions

File tree

packages/browser/src/client.ts

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,62 @@
1-
import { IBrowserClient, TraceoOptions } from "./types/client";
1+
import { IBrowserClient, TraceoBrowserError, TraceoOptions } from "./types/client";
22
import { Transport } from "./transport";
33
import { utils } from "./utils";
44
import { stacktrace } from "./exceptions/stacktrace";
55
import { Trace } from "./types/stacktrace";
66
import { BrowserIncidentType } from "./types/transport";
7+
import { EventOnErrorType, EventOnUnhandledRejectionType, windowEventHandlers } from "./handlers";
8+
import { BrowserInfoType } from "./types/browser";
79

810
export abstract class BrowserClient implements IBrowserClient {
911
public headers!: { [key: string]: any };
1012
public options: TraceoOptions;
1113
public transport: Transport;
14+
private browser: BrowserInfoType;
1215

1316
constructor(options: TraceoOptions) {
1417
this.options = options;
1518
this.transport = new Transport(this.options);
19+
this.browser = utils.browserDetails();
1620

1721
this.initSDK();
1822
}
1923

24+
/**
25+
* Method to implement dedicated logic only for a specific type of SDK.
26+
* Implement in dedicated client.
27+
*/
2028
public abstract postInitSDK(): void;
2129

22-
public sendError(error: Error): void {
30+
/**
31+
* Method to handle exceptions catched by ErrorBoundary component
32+
* Use this method via IBrowserClient interface implmeneted in dedicated clients
33+
* @param error
34+
*/
35+
public handleError(error: TraceoBrowserError): void {
2336
const err = this.constructError(error);
24-
this.transport.send<BrowserIncidentType>(err, this.headers);
37+
if (err) {
38+
this.transport.send<BrowserIncidentType>(err, this.headers);
39+
}
2540
}
2641

27-
private constructError(error: Error): BrowserIncidentType {
28-
const browser = utils.browserDetails();
42+
/**
43+
* Method to send errors catched by window event handlers like onerror or onunhandledrejection
44+
* @param error
45+
*/
46+
private sendError(error: BrowserIncidentType): void {
47+
this.transport.send<BrowserIncidentType>(error, this.headers);
48+
}
49+
50+
private constructError(error?: Error): BrowserIncidentType | null {
51+
if (!error) {
52+
console.debug("-- : --");
53+
return null;
54+
}
2955

30-
const type = error.name;
31-
const message = error.message;
56+
const type = error?.name || "Error";
57+
const message = error?.message || "(No message)";
3258

33-
const stack = error.stack || "";
59+
const stack = error?.stack || "";
3460

3561
const traces: Trace[] = stacktrace.parse(stack);
3662

@@ -39,13 +65,80 @@ export abstract class BrowserClient implements IBrowserClient {
3965
message,
4066
stack,
4167
traces,
42-
browser
68+
browser: this.browser
4369
};
4470

4571
return err;
4672
}
4773

4874
private initSDK(): void {
75+
if (this.isOffline) {
76+
return;
77+
}
78+
79+
if (window !== undefined) {
80+
this.initWindowEventHandlers();
81+
}
82+
4983
this.postInitSDK();
5084
}
85+
86+
private initWindowEventHandlers() {
87+
/**
88+
* Event handlers setup to catch exceptions via native js mechanism
89+
*/
90+
91+
windowEventHandlers["onerror"]((data: EventOnErrorType) => this.handleOnErrorEvent(data));
92+
windowEventHandlers["unhandledrejection"]((data: EventOnUnhandledRejectionType) =>
93+
this.handleOnUnhandledRejectionEvent(data)
94+
);
95+
}
96+
97+
private handleOnErrorEvent = (data: EventOnErrorType) => {
98+
if (data.error) {
99+
const eventError = this.constructError(data?.error);
100+
if (!eventError) {
101+
console.debug("-- --- --");
102+
return;
103+
}
104+
105+
if (data?.event) {
106+
eventError.type = data.event.toString();
107+
}
108+
109+
this.sendError(eventError);
110+
}
111+
};
112+
113+
private handleOnUnhandledRejectionEvent = (data: EventOnUnhandledRejectionType) => {
114+
// Without reason inside event there is no informations about catched exception
115+
if ("reason" in data.event) {
116+
const reason = data.event.reason;
117+
118+
const type = "Unhandled rejection";
119+
const message = reason.message;
120+
121+
let stack = "";
122+
if ("stack" in reason) {
123+
stack = reason.stack;
124+
} else {
125+
stack = reason.toString();
126+
}
127+
128+
const traces: Trace[] = stacktrace.parse(stack);
129+
const err: BrowserIncidentType = {
130+
type,
131+
message,
132+
stack,
133+
traces,
134+
browser: this.browser
135+
};
136+
137+
this.sendError(err);
138+
}
139+
};
140+
141+
private get isOffline(): boolean | undefined {
142+
return this.options.offline;
143+
}
51144
}

packages/browser/src/exceptions/stacktrace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const parse = (stackTrace: string): Trace[] => {
1212

1313
for (const line of lines) {
1414
const stackTraceLine = parseStackTraceLine(line);
15-
if (stackTraceLine) {
15+
if (stackTraceLine && stackTraceLine?.filename) {
1616
traces.push(stackTraceLine);
1717
}
1818
}

packages/browser/src/handlers.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export type WindowEvent = "unhandledrejection" | "onerror";
2+
3+
type EventCallback<T> = (data: T) => void;
4+
5+
export type EventOnUnhandledRejectionType = {
6+
window: WindowEventHandlers;
7+
event: PromiseRejectionEvent;
8+
};
9+
10+
export type EventOnErrorType = {
11+
event: Event | string;
12+
source?: string;
13+
lineno?: number;
14+
colno?: number;
15+
error?: Error;
16+
};
17+
18+
const handleUnhandledrejection = (callback: EventCallback<EventOnUnhandledRejectionType>) => {
19+
window.onunhandledrejection = function (this: WindowEventHandlers, ev: PromiseRejectionEvent) {
20+
callback({
21+
event: ev,
22+
window: this
23+
});
24+
};
25+
};
26+
27+
const handleOnError = (callback: EventCallback<EventOnErrorType>) => {
28+
window.onerror = function (
29+
event: Event | string,
30+
source?: string,
31+
lineno?: number,
32+
colno?: number,
33+
error?: Error
34+
) {
35+
callback({
36+
event,
37+
source,
38+
lineno,
39+
colno,
40+
error
41+
});
42+
};
43+
};
44+
45+
export const windowEventHandlers: Record<WindowEvent, Function> = {
46+
onerror: handleOnError,
47+
unhandledrejection: handleUnhandledrejection
48+
};

packages/browser/src/types/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ export interface TraceoOptions {
22
apiKey: string;
33
appId: string;
44
url: string;
5+
offline?: boolean;
56
}
67

78
export interface TraceoBrowserError extends Error {}
89

910
export interface IBrowserClient {
10-
sendError(error: TraceoBrowserError): void;
11+
handleError(error: TraceoBrowserError): void;
1112
}
1213

1314
export type Dictionary<T> = {

packages/browser/src/types/transport.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export type RequestOptions<T> = {
1717
};
1818

1919
export type BrowserIncidentType = {
20-
type: string;
21-
message: string;
22-
stack: string;
20+
type?: string;
21+
message?: string;
22+
stack?: string;
2323
browser: BrowserInfoType;
24-
traces: Trace[];
24+
traces?: Trace[];
2525
};

packages/react/src/ErrorBoundary.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoun
2828
onError(error, componentStack);
2929
}
3030

31-
traceo.sendError(error);
31+
traceo.handleError(error);
3232
this.setState({ error, stacktrace: componentStack });
3333
}
3434

0 commit comments

Comments
 (0)