Skip to content

Commit b53be6e

Browse files
committed
Refactor to support ErrorTelemetry in non-browser processes
1 parent 06dd582 commit b53be6e

3 files changed

Lines changed: 184 additions & 124 deletions

File tree

src/vs/platform/telemetry/browser/errorTelemetry.ts

Lines changed: 4 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -3,69 +3,12 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { binarySearch } from 'vs/base/common/arrays';
6+
import { toDisposable } from 'vs/base/common/lifecycle';
77
import { globals } from 'vs/base/common/platform';
8-
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
9-
import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
10-
import * as Errors from 'vs/base/common/errors';
11-
import { safeStringify } from 'vs/base/common/objects';
8+
import BaseErrorTelemetry, { ErrorEvent } from '../common/errorTelemetry';
129

13-
/* __GDPR__FRAGMENT__
14-
"ErrorEvent" : {
15-
"stack": { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" },
16-
"message" : { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" },
17-
"filename" : { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" },
18-
"callstack": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
19-
"msg" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
20-
"file" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
21-
"line": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true },
22-
"column": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true },
23-
"uncaught_error_name": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
24-
"uncaught_error_msg": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
25-
"count": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true }
26-
}
27-
*/
28-
interface ErrorEvent {
29-
callstack: string;
30-
msg?: string;
31-
file?: string;
32-
line?: number;
33-
column?: number;
34-
uncaught_error_name?: string;
35-
uncaught_error_msg?: string;
36-
count?: number;
37-
}
38-
39-
namespace ErrorEvent {
40-
export function compare(a: ErrorEvent, b: ErrorEvent) {
41-
if (a.callstack < b.callstack) {
42-
return -1;
43-
} else if (a.callstack > b.callstack) {
44-
return 1;
45-
}
46-
return 0;
47-
}
48-
}
49-
50-
export default class ErrorTelemetry {
51-
52-
public static ERROR_FLUSH_TIMEOUT: number = 5 * 1000;
53-
54-
private _telemetryService: ITelemetryService;
55-
private _flushDelay: number;
56-
private _flushHandle: any = -1;
57-
private _buffer: ErrorEvent[] = [];
58-
private _disposables: IDisposable[] = [];
59-
60-
constructor(telemetryService: ITelemetryService, flushDelay = ErrorTelemetry.ERROR_FLUSH_TIMEOUT) {
61-
this._telemetryService = telemetryService;
62-
this._flushDelay = flushDelay;
63-
64-
// (1) check for unexpected but handled errors
65-
const unbind = Errors.errorHandler.addListener((err) => this._onErrorEvent(err));
66-
this._disposables.push(toDisposable(unbind));
67-
68-
// (2) check for uncaught global errors
10+
export default class ErrorTelemetry extends BaseErrorTelemetry {
11+
protected installErrorListeners(): void {
6912
let oldOnError: Function;
7013
let that = this;
7114
if (typeof globals.onerror === 'function') {
@@ -84,37 +27,7 @@ export default class ErrorTelemetry {
8427
}));
8528
}
8629

87-
dispose() {
88-
clearTimeout(this._flushHandle);
89-
this._flushBuffer();
90-
this._disposables = dispose(this._disposables);
91-
}
92-
93-
private _onErrorEvent(err: any): void {
94-
95-
if (!err) {
96-
return;
97-
}
98-
99-
// unwrap nested errors from loader
100-
if (err.detail && err.detail.stack) {
101-
err = err.detail;
102-
}
103-
104-
// work around behavior in workerServer.ts that breaks up Error.stack
105-
let callstack = Array.isArray(err.stack) ? err.stack.join('\n') : err.stack;
106-
let msg = err.message ? err.message : safeStringify(err);
107-
108-
// errors without a stack are not useful telemetry
109-
if (!callstack) {
110-
return;
111-
}
112-
113-
this._enqueue({ msg, callstack });
114-
}
115-
11630
private _onUncaughtError(msg: string, file: string, line: number, column?: number, err?: any): void {
117-
11831
let data: ErrorEvent = {
11932
callstack: msg,
12033
msg,
@@ -138,37 +51,4 @@ export default class ErrorTelemetry {
13851

13952
this._enqueue(data);
14053
}
141-
142-
private _enqueue(e: ErrorEvent): void {
143-
144-
const idx = binarySearch(this._buffer, e, ErrorEvent.compare);
145-
if (idx < 0) {
146-
e.count = 1;
147-
this._buffer.splice(~idx, 0, e);
148-
} else {
149-
if (!this._buffer[idx].count) {
150-
this._buffer[idx].count = 0;
151-
}
152-
this._buffer[idx].count! += 1;
153-
}
154-
155-
if (this._flushHandle === -1) {
156-
this._flushHandle = setTimeout(() => {
157-
this._flushBuffer();
158-
this._flushHandle = -1;
159-
}, this._flushDelay);
160-
}
161-
}
162-
163-
private _flushBuffer(): void {
164-
for (let error of this._buffer) {
165-
/* __GDPR__
166-
"UnhandledError" : {
167-
"${include}": [ "${ErrorEvent}" ]
168-
}
169-
*/
170-
this._telemetryService.publicLog('UnhandledError', error, true);
171-
}
172-
this._buffer.length = 0;
173-
}
17454
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { binarySearch } from 'vs/base/common/arrays';
7+
import * as Errors from 'vs/base/common/errors';
8+
import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
9+
import { safeStringify } from 'vs/base/common/objects';
10+
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
11+
12+
/* __GDPR__FRAGMENT__
13+
"ErrorEvent" : {
14+
"stack": { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" },
15+
"message" : { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" },
16+
"filename" : { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" },
17+
"callstack": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
18+
"msg" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
19+
"file" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
20+
"line": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true },
21+
"column": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true },
22+
"uncaught_error_name": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
23+
"uncaught_error_msg": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
24+
"count": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true }
25+
}
26+
*/
27+
export interface ErrorEvent {
28+
callstack: string;
29+
msg?: string;
30+
file?: string;
31+
line?: number;
32+
column?: number;
33+
uncaught_error_name?: string;
34+
uncaught_error_msg?: string;
35+
count?: number;
36+
}
37+
38+
export namespace ErrorEvent {
39+
export function compare(a: ErrorEvent, b: ErrorEvent) {
40+
if (a.callstack < b.callstack) {
41+
return -1;
42+
} else if (a.callstack > b.callstack) {
43+
return 1;
44+
}
45+
return 0;
46+
}
47+
}
48+
49+
export default abstract class BaseErrorTelemetry {
50+
51+
public static ERROR_FLUSH_TIMEOUT: number = 5 * 1000;
52+
53+
private _telemetryService: ITelemetryService;
54+
private _flushDelay: number;
55+
private _flushHandle: any = -1;
56+
private _buffer: ErrorEvent[] = [];
57+
protected _disposables: IDisposable[] = [];
58+
59+
constructor(telemetryService: ITelemetryService, flushDelay = BaseErrorTelemetry.ERROR_FLUSH_TIMEOUT) {
60+
this._telemetryService = telemetryService;
61+
this._flushDelay = flushDelay;
62+
63+
// (1) check for unexpected but handled errors
64+
const unbind = Errors.errorHandler.addListener((err) => this._onErrorEvent(err));
65+
this._disposables.push(toDisposable(unbind));
66+
67+
// (2) install implementation-specific error listeners
68+
this.installErrorListeners();
69+
}
70+
71+
dispose() {
72+
clearTimeout(this._flushHandle);
73+
this._flushBuffer();
74+
this._disposables = dispose(this._disposables);
75+
}
76+
77+
protected installErrorListeners(): void {
78+
// to override
79+
}
80+
81+
private _onErrorEvent(err: any): void {
82+
83+
if (!err) {
84+
return;
85+
}
86+
87+
// unwrap nested errors from loader
88+
if (err.detail && err.detail.stack) {
89+
err = err.detail;
90+
}
91+
92+
// work around behavior in workerServer.ts that breaks up Error.stack
93+
let callstack = Array.isArray(err.stack) ? err.stack.join('\n') : err.stack;
94+
let msg = err.message ? err.message : safeStringify(err);
95+
96+
// errors without a stack are not useful telemetry
97+
if (!callstack) {
98+
return;
99+
}
100+
101+
this._enqueue({ msg, callstack });
102+
}
103+
104+
protected _enqueue(e: ErrorEvent): void {
105+
106+
const idx = binarySearch(this._buffer, e, ErrorEvent.compare);
107+
if (idx < 0) {
108+
e.count = 1;
109+
this._buffer.splice(~idx, 0, e);
110+
} else {
111+
if (!this._buffer[idx].count) {
112+
this._buffer[idx].count = 0;
113+
}
114+
this._buffer[idx].count! += 1;
115+
}
116+
117+
if (this._flushHandle === -1) {
118+
this._flushHandle = setTimeout(() => {
119+
this._flushBuffer();
120+
this._flushHandle = -1;
121+
}, this._flushDelay);
122+
}
123+
}
124+
125+
private _flushBuffer(): void {
126+
for (let error of this._buffer) {
127+
/* __GDPR__
128+
"UnhandledError" : {
129+
"${include}": [ "${ErrorEvent}" ]
130+
}
131+
*/
132+
this._telemetryService.publicLog('UnhandledError', error, true);
133+
}
134+
this._buffer.length = 0;
135+
}
136+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { onUnexpectedError } from 'vs/base/common/errors';
7+
import BaseErrorTelemetry from '../common/errorTelemetry';
8+
9+
export default class ErrorTelemetry extends BaseErrorTelemetry {
10+
protected installErrorListeners(): void {
11+
// Print a console message when rejection isn't handled within N seconds. For details:
12+
// see https://nodejs.org/api/process.html#process_event_unhandledrejection
13+
// and https://nodejs.org/api/process.html#process_event_rejectionhandled
14+
const unhandledPromises: Promise<any>[] = [];
15+
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
16+
unhandledPromises.push(promise);
17+
setTimeout(() => {
18+
const idx = unhandledPromises.indexOf(promise);
19+
if (idx >= 0) {
20+
promise.catch(e => {
21+
unhandledPromises.splice(idx, 1);
22+
console.warn(`rejected promise not handled within 1 second: ${e}`);
23+
if (e.stack) {
24+
console.warn(`stack trace: ${e.stack}`);
25+
}
26+
onUnexpectedError(reason);
27+
});
28+
}
29+
}, 1000);
30+
});
31+
32+
process.on('rejectionHandled', (promise: Promise<any>) => {
33+
const idx = unhandledPromises.indexOf(promise);
34+
if (idx >= 0) {
35+
unhandledPromises.splice(idx, 1);
36+
}
37+
});
38+
39+
// Print a console message when an exception isn't handled.
40+
process.on('uncaughtException', (err: Error) => {
41+
onUnexpectedError(err);
42+
});
43+
}
44+
}

0 commit comments

Comments
 (0)