-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathcatcher.ts
More file actions
293 lines (254 loc) · 8.62 KB
/
Copy pathcatcher.ts
File metadata and controls
293 lines (254 loc) · 8.62 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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
import './modules/element-sanitizer';
import Socket from './modules/socket';
import type { HawkInitialSettings } from './types';
import { isPerformanceIssueDetectorEnabled } from './types/issues';
import { VueIntegration } from './integrations/vue';
import type { VueIntegrationAddons } from '@hawk.so/types';
import type { JavaScriptCatcherIntegrations } from '@/types';
import { ConsoleCatcher } from './addons/consoleCatcher';
import { BrowserBreadcrumbStore } from './addons/breadcrumbs';
import { BaseCatcher, HawkUserManager, isLoggerSet, log, setLogger, decodeIntegrationId, EventDedupeTransport } from '@hawk.so/core';
import { HawkLocalStorage } from './utils/hawk-local-storage';
import { createBrowserLogger } from './utils/logger';
import { BrowserRandomGenerator } from './utils/random';
import { BrowserAddonMessageProcessor } from './addons/browser-addon-message-processor';
import { ConsoleOutputAddonMessageProcessor } from './addons/console-output-addon-message-processor';
import { DebugAddonMessageProcessor } from './addons/debug-addon-message-processor';
import { BrowserBreadcrumbsMessageProcessor } from './addons/browser-breadcrumbs-message-processor';
import { PerformanceIssuesMonitor } from './addons/performance-issues';
/**
* Allow to use global VERSION, that will be overwritten by Webpack
*/
declare const VERSION: string;
/**
* Registers a global logger instance if not already done.
*/
if (!isLoggerSet()) {
setLogger(createBrowserLogger(VERSION));
}
/**
* Hawk JavaScript browser catcher
* Module for errors and exceptions tracking
*
* @copyright CodeX
*/
export default class Catcher extends BaseCatcher<typeof Catcher.type> {
/**
* JS Catcher version
*/
public readonly version: string = VERSION;
/**
* Vue.js integration instance
*/
public vue: VueIntegration | null = null;
/**
* Catcher Type
*/
private static readonly type = 'errors/javascript' as const;
/**
* Enable debug mode
*/
private readonly debug: boolean;
/**
* Disable Vue.js error handler
*/
private readonly disableVueErrorHandler: boolean = false;
/**
* Console log handler
*/
private readonly consoleTracking: boolean;
/**
* Console catcher instance
*/
private readonly consoleCatcher: ConsoleCatcher | null = null;
/**
* Issues monitor instance
*/
private issuesMonitor: PerformanceIssuesMonitor | null = null;
/**
* Catcher constructor
*
* @param {HawkInitialSettings|string} settings - If settings is a string, it means an Integration Token
*/
constructor(settings: HawkInitialSettings | string) {
if (typeof settings === 'string') {
settings = {
token: settings,
} as HawkInitialSettings;
}
const token = settings.token;
const userManager = new HawkUserManager(
new HawkLocalStorage(),
new BrowserRandomGenerator()
);
/**
* Init transport
* WebSocket decorator by default, or custom via {@link settings.transport}
* No-op when token is missing
*/
const transport = !token
? { send: (): Promise<void> => Promise.resolve() }
: settings.transport ?? new Socket({
collectorEndpoint: settings.collectorEndpoint || `wss://${decodeIntegrationId(token)}.k1.hawk.so:443/ws`,
connectionIdleMs: settings.reconnectionTimeout,
onClose(): void {
log(
'Connection lost. Connection will be restored when new errors occurred',
'info'
);
},
});
const batcher = new EventDedupeTransport(transport);
// Flush buffered events before the socket closes on page hide
window.addEventListener('pagehide', () => batcher.flush(), { capture: true });
let breadcrumbStore: BrowserBreadcrumbStore | null = null;
if (token && settings.breadcrumbs !== false) {
breadcrumbStore = BrowserBreadcrumbStore.getInstance();
}
super(
token,
batcher,
userManager,
settings.release !== undefined ? String(settings.release) : undefined,
settings.context || undefined,
settings.beforeSend,
breadcrumbStore ?? undefined
);
this.debug = settings.debug || false;
if (settings.user) {
this.setUser(settings.user);
}
this.disableVueErrorHandler =
settings.disableVueErrorHandler !== null && settings.disableVueErrorHandler !== undefined
? settings.disableVueErrorHandler
: false;
this.consoleTracking =
settings.consoleTracking !== null && settings.consoleTracking !== undefined
? settings.consoleTracking
: true;
if (!token) {
log(
'Integration Token is missed. You can get it on https://hawk.so at Project Settings.',
'warn'
);
return;
}
this.addMessageProcessor(new BrowserAddonMessageProcessor());
if (this.consoleTracking) {
this.consoleCatcher = ConsoleCatcher.getInstance();
this.addMessageProcessor(new ConsoleOutputAddonMessageProcessor(this.consoleCatcher));
}
// Initialize breadcrumbs
if (settings.breadcrumbs !== false) {
this.addMessageProcessor(new BrowserBreadcrumbsMessageProcessor(settings.breadcrumbs ?? {}));
}
if (this.debug) {
this.addMessageProcessor(new DebugAddonMessageProcessor());
}
this.configureIssues(settings);
if (settings.vue) {
this.connectVue(settings.vue);
}
}
/**
* Method for Frameworks SDK using own error handlers.
* Allows to send errors to Hawk with additional Frameworks data (addons)
*
* @param error - error to send
* @param addons - framework-specific data, can be undefined
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public captureError(error: Error | string, addons?: JavaScriptCatcherIntegrations): void {
void this.formatAndSend(error, addons);
}
/**
* Add error handing to the passed Vue app
*
* @param vue - Vue app
*/
public connectVue(vue): void {
// eslint-disable-next-line no-new
this.vue = new VueIntegration(
vue,
(error: Error, addons: VueIntegrationAddons) => {
void this.formatAndSend(error, {
vue: addons,
});
},
{
disableVueErrorHandler: this.disableVueErrorHandler,
}
);
}
/**
* Returns {@link Catcher.type}
*/
protected getCatcherType(): typeof Catcher.type {
return Catcher.type;
}
/**
* Returns catcher version
*/
protected getCatcherVersion(): string {
return VERSION;
}
/**
* Configure issues-related features:
* - global errors handling
* - performance issue detectors (Long Tasks / LoAF)
*
* @param settings
*/
private configureIssues(settings: HawkInitialSettings): void {
if (settings.issues === false) {
return;
}
const issues = settings.issues ?? {};
const shouldHandleGlobalErrors = settings.disableGlobalErrorsHandling !== true && issues.errors !== false;
const shouldDetectPerformanceIssues = isPerformanceIssueDetectorEnabled(issues.longTasks)
|| isPerformanceIssueDetectorEnabled(issues.longAnimationFrames)
|| isPerformanceIssueDetectorEnabled(issues.webVitals);
if (shouldHandleGlobalErrors) {
this.initGlobalHandlers();
}
if (shouldDetectPerformanceIssues) {
this.issuesMonitor ??= new PerformanceIssuesMonitor();
this.issuesMonitor.init(issues, (entry) => {
void this.formatAndSend(entry.title, entry.addons);
});
}
}
/**
* Init global errors handler
*/
private initGlobalHandlers(): void {
window.addEventListener('error', (event: ErrorEvent) => this.handleEvent(event));
window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => this.handleEvent(event));
}
/**
* Handles the event and sends it to the server
*
* @param {ErrorEvent|PromiseRejectionEvent} event — (!) both for Error and Promise Rejection
*/
private async handleEvent(event: ErrorEvent | PromiseRejectionEvent): Promise<void> {
// Add error to console logs
if (this.consoleTracking) {
this.consoleCatcher!.addErrorEvent(event);
}
/**
* Promise rejection reason is recommended to be an Error, but it can be a string:
* - Promise.reject(new Error('Reason message')) ——— recommended
* - Promise.reject('Reason message')
*/
let error = (event as ErrorEvent).error || (event as PromiseRejectionEvent).reason;
/**
* Case when error triggered in external script
* We can't access event error object because of CORS
* Event message will be 'Script error.'
*/
if (event instanceof ErrorEvent && error === undefined) {
error = (event as ErrorEvent).message;
}
void this.formatAndSend(error);
}
}