bug-reporterjs-lib
Version:
A lightweight and framework-agnostic JavaScript bug reporter with screenshot capture, error logging, and Axios error tracking. Designed to work seamlessly in Vue, React, and plain JavaScript projects.
558 lines (531 loc) • 17.7 kB
JavaScript
async function E() {
const m = await import("./index-D-6arYiK.mjs"), e = document.body;
return await m.toJpeg(e, {
quality: 1,
pixelRatio: 1,
skipAutoScale: !0,
width: e.clientWidth,
height: e.clientHeight
});
}
const _ = "CLIENT_ERROR_LOGS", b = "LAST_ERROR_REQUEST", w = "http://p-c-ers.asakabank.com/report", v = {
title: "Report a Bug",
leave_comment: "Leave a comment",
send: "Send",
noClientErrors: "No client errors",
noNetworkErrors: "No network errors",
report_success_sent: "Message successfully sent",
report_failed: "Failed to send message, please try again later",
jira_creds_required: "JIRA credentials are required",
username: "Username",
password: "Password",
continue: "Continue",
enter_jira_creds: "Enter JIRA Credentials"
}, y = {
title: "Сообщить об ошибке",
leave_comment: "Оставить комментарий",
send: "Отправить",
noClientErrors: "Нет клиентских ошибок",
noNetworkErrors: "Нет сетевых ошибок",
report_success_sent: "Cообщение успешно отправлено",
report_failed: "Не удалось отправить сообщение, пожалуйста, попробуйте позже",
jira_creds_required: "JIRA учетные данные обязательны",
username: "Имя пользователя",
password: "Пароль",
continue: "Продолжить",
enter_jira_creds: "Введите учетные данные JIRA"
}, q = {
title: "Xatolik haqida xabar berish",
leave_comment: "Izoh qoldirish",
send: "Jo'natish",
noClientErrors: "Mijoz xatoliklari yo'q",
noNetworkErrors: "Tarmoq xatoliklari yo'q",
report_success_sent: "Xabar muvaffaqiyatli yuborildi",
report_failed: "Xabar yuborishda xato, iltimos keyinroq qaytadan urinib ko'ring",
jira_creds_required: "JIRA foydalanuvchi ma'lumotlari talab qilinadi",
username: "Foydalanuvchi nomi",
password: "Parol",
continue: "Davom etish",
enter_jira_creds: "JIRA foydalanuvchi ma'lumotlarini kiriting"
}, R = {
title: "Хатолик ҳақида хабар бериш",
leave_comment: "Изоҳ қолдириш",
send: "Жўнатиш",
noClientErrors: "Мижоз хатоликлари йўқ",
noNetworkErrors: "Тармоқ хатоликлари йўқ",
report_success_sent: "Хабар муваффақиятли юборилди",
report_failed: "Хабарни юборишда хатолик юз берди, илтимос, кейинроқ қайта уриниб кўринг",
jira_creds_required: "JIRA фойдаланувчи маълумотлари талаб қилинади",
username: "Фойдаланувчи номи",
password: "Пароль",
continue: "Давом этиш",
enter_jira_creds: "JIRA фойдаланувчи маълумотларини киритинг"
}, x = (m) => {
let e = {};
switch (m) {
case "uz":
e = R;
break;
case "o'z":
e = q;
break;
case "en":
e = v;
break;
case "ru":
e = y;
default:
e = y;
}
return (r) => e[r] || r;
};
function S(m, e, r, a) {
const i = x(r), o = document.createElement("div");
document.body.style.overflow = "hidden", o.id = "bug-modal", o.innerHTML = `
<style>
#bug-modal {
position: fixed;
top: 0; left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
#bug-modal * {
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
#bug-modal .modal-content {
background: #fff;
width: 80%;
max-width: 95vw;
padding: 30px;
border-radius: 30px;
box-shadow: 0px 2px 6px 0px #0000001F, 0px 0px 2px 0px #0000000F, 0px 4px 10px 0px #00000008;
font-family: sans-serif;
display: flex;
flex-direction: column;
gap:30px;
animation: fadeIn 0.2s ease-in-out;
}
#bug-modal form {
display: flex;
flex-direction: column;
gap:16px;
}
#bug-modal .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
}
#bug-modal .modal-header span{
cursor: pointer;
font-size: 24px;
color: #333;
transition: all 0.2s;
line-height: 1;
}
#bug-modal h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
#bug-modal h3 {
margin: 0;
font-size: 20px;
color: #065cb9;
text-align: center;
}
#bug-modal img {
width: 100%;
max-height: 400px;
object-fit: cover;
object-position: top;
border: 1px solid #CED4DA;
}
#bug-modal textarea {
padding: 15px;
font-size: 14px;
border: 1px solid #CED4DA;
border-radius: 30px;
outline: none;
resize: none;
}
#bug-modal input {
padding: 15px;
font-size: 14px;
border: 1px solid #CED4DA;
border-radius: 30px;
outline: none;
resize: none;
}
#bug-modal textarea:hover,#bug-modal textarea:focus {
border-color: #232323;
}
#bug-modal .button-container {
display: flex;
justify-content: center;
}
#bug-modal button {
padding: 12px 24px;
height: 54px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
background: #333333;
color: white;
border: none;
border-radius: 30px;
gap: 10px;
cursor: pointer;
transition: all 0.2s;
min-width: 150px;
}
#bug-modal button:hover {
background: #232323;
box-shadow: 0px 5px 5px 0px #00000040;
}
#bug-modal button:disabled {
background: #E5E5E5;
cursor: not-allowed;
color: #6C757D;
}
#bug-modal .loader {
width: 24px;
height: 24px;
border-radius: 50%;
position: relative;
animation: rotate 1s linear infinite
}
#bug-modal .loader.hide {
display: none;
}
#bug-modal .jira-creds {
display: flex;
flex-direction: column;
gap:16px;
}
#bug-modal .loader::before {
content: "";
box-sizing: border-box;
position: absolute;
inset: 0px;
border-radius: 50%;
border: 3px solid #6C757D;
animation: prixClipFix 2s linear infinite ;
}
@keyframes rotate {
100% {transform: rotate(360deg)}
}
@keyframes prixClipFix {
0% {clip-path:polygon(50% 50%,0 0,0 0,0 0,0 0,0 0)}
25% {clip-path:polygon(50% 50%,0 0,100% 0,100% 0,100% 0,100% 0)}
50% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,100% 100%,100% 100%)}
75% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 100%)}
100% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 0)}
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
</style>
<div class="modal-content">
<div class="modal-header">
<h2>${i("title")}</h2>
<span id="close-modal">×</span>
</div>
<form id="bug-modal-form" class="report-form">
<img src="${m}" alt="Screenshot"/>
<input type="tel" pattern="^+998(9[0-9]|8[1-9]|7[1-9])[0-9]{7}$" placeholder="+998901234567" id="bug-phone" />
<textarea id="bug-comment" rows="3" placeholder="${i(
"leave_comment"
)}..."></textarea>
<div class="button-container">
<button ${a ? "" : 'id="send-btn"'} type="submit">
${a ? "" : '<span id="bug-loader" class="loader hide"></span>'}
<span>${i(a ? "continue" : "send")}</span>
</button>
</div>
</form>
${a ? `
<form id="bug-jira-creds-form" class="report-form" style="display:none;">
<h3>${i("enter_jira_creds")}</h3>
<div class="jira-creds">
<input type="text" id="bug-username" required placeholder="${i(
"username"
)}" />
<input type="password" id="bug-password" required placeholder="${i(
"password"
)}" />
</div>
<div class="button-container">
<button id="send-btn" type="submit">
<span id="bug-loader" class="loader hide"></span>
<span>${i("send")}</span>
</button>
</div>
</form>
` : ""}
</div>
`, document.body.appendChild(o);
const t = document.querySelectorAll(".report-form");
let u = !1;
const d = () => {
o.remove(), sessionStorage.removeItem(_), sessionStorage.removeItem(b), document.body.style.overflow = "auto";
};
document.getElementById("close-modal")?.addEventListener("click", () => {
d();
});
const s = async () => {
const n = document.getElementById("send-btn"), l = document.getElementById("bug-loader"), f = document.getElementById("bug-comment")?.value, p = document.getElementById("bug-phone")?.value, g = document.getElementById("bug-username")?.value, h = document.getElementById("bug-password")?.value;
if (console.log(g, h), a && (!g || !h)) {
alert(i("jira_creds_required"));
return;
}
n?.setAttribute("disabled", "true"), l?.classList.remove("hide"), await e({
comment: f,
phone: p,
...a ? { username: g, password: h } : {}
}), n?.removeAttribute("disabled"), l?.classList.add("hide"), d();
};
t.forEach((n) => {
n?.addEventListener("submit", async (l) => {
if (l.preventDefault(), console.log(u), a && !u) {
u = !0;
const f = document.getElementById("bug-jira-creds-form"), p = document.getElementById("bug-modal-form");
f && (f.style.display = "flex"), p && (p.style.display = "none");
return;
}
console.log(1), s();
});
}), document.getElementById("bug-phone")?.addEventListener("input", (n) => {
let l = n.target?.value.replace(/[^\d+]/g, "");
l.includes("+") && (l = "+" + l.replace(/\+/g, "")), l.length > 13 && (l = l.slice(0, 13)), n.target && (n.target.value = l);
});
}
class k {
project;
isJiraCredsRequired;
locale;
constructor(e) {
this.project = e.project, this.isJiraCredsRequired = e.isJiraCredsRequired || !1, this.setupErrorListeners(), this.setupGlobalErrorListeners();
}
setupErrorListeners() {
const e = window.fetch;
window.fetch = async (...o) => {
try {
const t = await e(...o);
if (!t.ok || !t.status) {
const d = await t.clone().text();
this.storeNetworkError({
request_url: typeof o[0] == "string" ? o[0] : o[0] instanceof Request ? o[0].url : o[0] instanceof URL ? o[0].toString() : void 0,
request_header: o[1]?.headers || {},
request_status_code: t.status,
response_data: (() => {
try {
const s = JSON.parse(d);
return typeof s == "object" && s !== null ? s : { message: d };
} catch {
return { message: d };
}
})()
});
}
return t;
} catch (t) {
throw this.storeNetworkError({
request_url: typeof o[0] == "string" ? o[0] : o[0] instanceof Request ? o[0].url : o[0] instanceof URL ? o[0].toString() : void 0,
response_data: { message: t?.message || t.toString() }
}), t;
}
};
const r = XMLHttpRequest.prototype.open, a = XMLHttpRequest.prototype.send, i = this;
XMLHttpRequest.prototype.open = function(o, t) {
return this._bug_report_url = t, r.apply(this, arguments);
}, XMLHttpRequest.prototype.send = function(o) {
const t = this, u = (s) => {
if (s) {
if (typeof s == "string")
try {
return JSON.parse(s);
} catch {
return s;
}
else if (s instanceof FormData) {
const c = {};
for (const [n, l] of s.entries())
c[n] = l;
return c;
} else if (s instanceof URLSearchParams) {
const c = {};
for (const [n, l] of s.entries())
c[n] = l;
return c;
} else {
if (s instanceof Blob)
return "[Blob]";
if (typeof s == "object")
return s;
}
return String(s);
}
}, d = () => {
const s = t.status;
if (!(s >= 200 && s < 300)) {
let c;
try {
const n = JSON.parse(t.responseText);
n && typeof n == "object" && !Array.isArray(n) ? c = n : c = n ? { message: n } : { message: "Unknown error" };
} catch {
c = t.responseText ? { message: t.responseText } : { message: "Unknown error" };
}
try {
const n = {};
t.getAllResponseHeaders().trim().split(/[\r\n]+/).forEach((f) => {
const p = f.split(": "), g = p.shift(), h = p.join(": ");
g && (n[g] = h);
}), i.storeNetworkError({
request_url: t._bug_report_url,
request_payload: u(o),
request_header: n,
request_status_code: s,
response_data: c
});
} catch {
i.storeNetworkError({
request_url: t._bug_report_url,
request_status_code: s,
response_data: { message: t.responseText }
});
}
}
t.removeEventListener("loadend", d);
};
return t.addEventListener("loadend", d), a.call(t, o);
};
}
isHttpError(e) {
const r = e?.message || e?.toString?.() || (typeof e == "string" ? e : "");
return [
"Load failed",
"fetch",
"Network Error",
"Request failed",
"Failed to fetch",
"status code",
"timeout",
"axios",
"net::ERR",
"Failed to load resource",
"A server with the specified hostname could not be found",
"Failed to fetch",
"ERR_CONNECTION_REFUSED",
"ERR_NAME_NOT_RESOLVED"
].some((i) => r.includes(i));
}
setupGlobalErrorListeners() {
window.onerror = (e, r, a, i, o) => {
this.isHttpError(o || e) || this.storeConsoleError({
data: {
message: o?.message || String(e),
source: r,
line: a,
column: i
}
});
}, window.onunhandledrejection = (e) => {
const r = e?.reason;
this.isHttpError(r) || this.storeConsoleError({
data: {
message: r?.message || r?.toString?.() || "Unhandled rejection"
}
});
};
}
storeNetworkError(e) {
const r = {
request_status_code: e.request_status_code,
request_url: e.request_url || "",
location_url: window.location.href,
request_header: e.request_header ?? {},
request_payload: e.request_payload ?? {},
response_data: e.response_data ?? {}
};
sessionStorage.setItem(b, JSON.stringify(r));
}
storeConsoleError(e) {
let r = JSON.parse(
sessionStorage.getItem(_) || "[]"
);
const a = {
location_url: window.location.href,
console_error: e.data
};
sessionStorage.setItem(
_,
JSON.stringify([...r, a].slice(-10))
);
}
async openModal({ callback: e, locale: r }) {
this.locale = r;
const a = await E();
S(
a,
(i) => this.sendReport({ image: a, ...i }, e),
this.locale,
this.isJiraCredsRequired
);
}
collectData({
comment: e,
image: r,
phone: a,
password: i,
username: o
}) {
const t = {
project_name: this.project,
user_agent: navigator.userAgent,
image: r,
location_url: window.location.href,
request_header: {},
request_payload: {},
response_data: {},
request_status_code: 0,
request_url: "",
console_error: { data: [] },
comment: e,
phone: a,
username: o,
password: i
}, u = JSON.parse(
sessionStorage.getItem(_) || "[]"
), d = JSON.parse(
sessionStorage.getItem(b) || "{}"
);
return u.length > 0 && (t.console_error = {
data: u.filter((s) => s.location_url === window.location.href).map((s) => s.console_error).filter((s) => s !== void 0)
}), d.location_url === window.location.href && (t.request_url = d.request_url, t.request_header = d.request_header, t.request_status_code = d.request_status_code, t.request_payload = d.request_payload, t.response_data = d.response_data), t;
}
async sendReport(e, r) {
const a = x(this.locale), i = this.collectData(e);
try {
const o = await fetch(w, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(i)
}), t = await o.json();
o.ok ? (r ? r(!0) : alert(a("report_success_sent")), sessionStorage.removeItem(b)) : (console.error("Server returned error response:", t), r ? r(!1) : alert(a("report_failed")));
} catch (o) {
console.error("Network or other error:", o), r ? r(!1) : alert(a("report_failed"));
}
}
}
export {
k as BugReporter
};