Files
hockey_new/hockey_data/static/client-performance.js

71 lines
3.1 KiB
JavaScript

(() => {
"use strict";
if (window.HockeyPerformance) return;
const metrics = { script_started_ms: performance.now() };
let errors = 0;
let readySent = false;
function post(event, message = "", values = {}) {
const payload = {
event,
build: String(window.UI_BUILDER_BOOT?.buildVersion || ""),
message: String(message).slice(0, 2000),
path: location.pathname,
metrics: values,
};
// A diagnostic must not slow first paint or produce a diagnostic loop.
fetch("/api/hockey/client-events", {
method: "POST", credentials: "same-origin", cache: "no-store", keepalive: true,
headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload),
}).catch(() => {});
}
window.HockeyPerformance = {
mark(name, value) { if (Number.isFinite(value)) metrics[name] = value; },
async measure(name, work) {
const start = performance.now();
try { return await work(); }
finally { metrics[name] = performance.now() - start; }
},
getMetrics() { return { ...metrics }; },
reportError(message) { if (errors++ < 10) post("error", message); },
};
window.addEventListener("hockey:runtime-ready", () => {
if (readySent) return;
readySent = true;
metrics.runtime_ready_ms = performance.now();
const navigation = performance.getEntriesByType("navigation")[0];
if (navigation) {
metrics.server_response_ms = Math.max(0, navigation.responseStart - navigation.requestStart);
metrics.document_download_ms = Math.max(0, navigation.responseEnd - navigation.responseStart);
metrics.connection_ms = Math.max(0, navigation.connectEnd - navigation.connectStart);
metrics.dom_interactive_ms = Math.max(0, navigation.domInteractive);
}
const resources = performance.getEntriesByType("resource");
metrics.resource_count = resources.length;
metrics.transfer_bytes = resources.reduce((sum, item) => sum + (item.transferSize || 0), 0);
metrics.decoded_bytes = resources.reduce((sum, item) => sum + (item.decodedBodySize || 0), 0);
metrics.cached_resources = resources.filter(item => item.transferSize === 0 && item.decodedBodySize > 0).length;
const send = () => {
metrics.first_ready_paint_ms = performance.now();
post("ready", "Интерфейс готов к работе", { ...metrics });
};
// Two frames include a paint, with a fallback for an inactive browser tab.
let sent = false;
const once = () => { if (!sent) { sent = true; send(); } };
requestAnimationFrame(() => requestAnimationFrame(once));
setTimeout(once, 1000);
});
window.addEventListener("error", event => {
if (errors++ >= 10) return;
const file = String(event.filename || "").split("?")[0];
post("error", `${event.message || "Ошибка браузера"} · ${file}:${event.lineno || 0}:${event.colno || 0}`);
});
window.addEventListener("unhandledrejection", event => {
if (errors++ >= 10) return;
post("unhandledrejection", String(event.reason?.message || event.reason || "Необработанная ошибка"));
});
})();