@w0s/report-js-error
Version:
Send script error information to endpoints
107 lines • 4.24 kB
JavaScript
/**
* Send script error information to endpoints
*/
export default class {
#endpoint; // URL of the endpoint
#options; // Information such as transmission conditions
/**
* @param endpoint - URL of the endpoint
* @param options - Information such as transmission conditions
*/
constructor(endpoint, options) {
this.#endpoint = endpoint;
this.#options = options;
if (!this.#checkUserAgent()) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
window.addEventListener('error', this.#errorEvent.bind(this), { passive: true });
}
/**
* ユーザーエージェントがレポートを行う対象かどうかチェックする
*
* @returns 対象なら true
*/
#checkUserAgent() {
const ua = navigator.userAgent;
const { denyUAs, allowUAs } = this.#options;
if (denyUAs?.some((denyUA) => denyUA.test(ua))) {
console.info('No JavaScript error report will be sent because the user agent match the deny list.');
return false;
}
if (allowUAs !== undefined && !allowUAs.some((allowUA) => allowUA.test(ua))) {
console.info('No JavaScript error report will be sent because the user agent does not match the allow list.');
return false;
}
return true;
}
/**
* エラーイベント
*
* @param ev - ErrorEvent
*/
async #errorEvent(ev) {
const { message, filename, lineno, colno } = ev;
if (filename === '') {
/* 2020年11月現在、「YJApp-ANDROID jp.co.yahoo.android.yjtop/3.81.0」と名乗るブラウザがこのような挙動を行う(fillename === '' && lineno === 0 && colno === 0) */
console.error('`ErrorEvent.filename` is empty.');
return;
}
const { denyFilenames, allowFilenames } = this.#options;
if (denyFilenames?.some((denyFilename) => denyFilename.test(filename))) {
console.info('No JavaScript error report will be sent because the filename match the deny list.');
return;
}
if (allowFilenames !== undefined && !allowFilenames.some((allowFilename) => allowFilename.test(filename))) {
console.info('No JavaScript error report will be sent because the filename does not match the allow list.');
return;
}
switch (new URL(filename).protocol) {
case 'https:':
case 'http:':
break;
default:
console.error('A JavaScript error has occurred in a non-HTTP protocol (This may be due to a browser extension).');
return;
}
await this.#fetch(message, filename, lineno, colno);
}
/**
* レポートを送信
*
* @param message - ErrorEvent.message
* @param filename - ErrorEvent.filename
* @param lineno - ErrorEvent.lineno
* @param colno - ErrorEvent.colno
*/
async #fetch(message, filename, lineno, colno) {
const { fetchParam, fetchContentType, fetchHeaders } = this.#options;
const headers = new Headers(fetchHeaders);
if (fetchContentType !== undefined) {
headers.set('Content-Type', fetchContentType);
}
const bodyObject = {
[fetchParam.documentURL]: location.toString(),
[fetchParam.message]: message,
[fetchParam.filename]: filename,
[fetchParam.lineno]: lineno,
[fetchParam.colno]: colno,
};
let body;
if (fetchContentType === 'application/json') {
body = JSON.stringify(bodyObject);
}
else {
body = new URLSearchParams(Object.fromEntries(Object.entries(bodyObject).map(([key, value]) => [key, String(value)])));
}
const response = await fetch(this.#endpoint, {
method: 'POST',
headers: headers,
body: body,
});
if (!response.ok) {
throw new Error(`"${response.url}" is ${String(response.status)} ${response.statusText}`);
}
}
}
//# sourceMappingURL=ReportJsError.js.map