@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
273 lines (242 loc) • 9.59 kB
JavaScript
/**
* Patches console methods to capture log messages and send them to the server.
* This is useful for debugging and logging in the client.
* @param {"log" | "warn" | "info" | "debug" | "error" | "internal"} level
* @param {any} message - The log message to capture.
*/
function sendLogToServer(level, ...message) {
if ("hot" in import.meta) {
message = stringifyLog(message);
// keep messages below payload limit
if(message.length > 100_000) {
message = message.slice(0, 100_000) + "... <truncated>";
}
// @ts-ignore
import.meta.hot.send("needle:client-log", { level, message: message });
}
}
// const obj = {
// hello: "world"
// }
// obj["test"] = obj;
// sendLogToServer("internal", "Test circular reference", obj);
if (import.meta && "hot" in import.meta) {
const originalLog = console.log;
const originalWarn = console.warn;
const originalInfo = console.info;
const originalDebug = console.debug;
const originalError = console.error;
console.log = (...args) => {
originalLog(...args);
sendLogToServer("log", ...args);
}
console.warn = (...args) => {
originalWarn(...args);
sendLogToServer("warn", ...args);
}
console.info = (...args) => {
originalInfo(...args);
sendLogToServer("info", ...args);
}
console.debug = (...args) => {
originalDebug(...args);
sendLogToServer("debug", ...args);
}
console.error = (...args) => {
originalError(...args);
sendLogToServer("error", ...args);
}
try {
sendLogToServer("internal", `Page loaded
URL: ${window.location.href}
UserAgent: ${navigator.userAgent}
Screen: ${window.innerWidth} x ${window.innerHeight}px
Device Pixel Ratio: ${window.devicePixelRatio}
Device Memory: ${"deviceMemory" in navigator ? navigator.deviceMemory : "Not available"} GB
Online: ${navigator.onLine}
Language: ${navigator.language}
Timezone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}
Connection: ${"connection" in navigator ? JSON.stringify(navigator.connection) : "Not available"}
User Activation: ${"userActivation" in navigator ? JSON.stringify(navigator.userActivation) : "Not available"}
`);
if ("gpu" in navigator) {
// @ts-ignore
navigator.gpu.requestAdapter()
.then(adapter => adapter ? adapter.requestDevice() : null)
.then(device => {
if (device) {
const adapterInfo = device.adapterInfo;
if (adapterInfo) {
sendLogToServer("internal", [`WebGPU adapter info`, {
vendor: adapterInfo.vendor,
architecture: adapterInfo.architecture,
device: adapterInfo.device,
description: adapterInfo.description,
features: adapterInfo.features,
limits: adapterInfo.limits
}]);
}
}
});
}
}
catch (e) {
// silently fail
sendLogToServer("error", `Error during initial log: ${e.message}`);
}
window.addEventListener('error', (event) => {
const errorMessage = event.error ? event.error.stack || event.error.message : event.message;
sendLogToServer("error", errorMessage);
});
window.addEventListener('unhandledrejection', (event) => {
const reason = event.reason ? event.reason.stack || event.reason.message : "Unhandled rejection without reason";
sendLogToServer("error", `Unhandled promise rejection: ${reason}`);
});
window.addEventListener('beforeunload', () => {
sendLogToServer("internal", "Page is unloading");
});
document.addEventListener('visibilitychange', () => {
console.log("Visibility changed:", document.visibilityState);
if (document.visibilityState === 'hidden') {
sendLogToServer("internal", "Page is hidden");
}
else if (document.visibilityState === 'visible') {
sendLogToServer("internal", "Page is visible again");
}
else {
sendLogToServer("internal", `Page visibility changed to ${document.visibilityState}`);
}
});
window.addEventListener("focus", () => {
sendLogToServer("internal", "Page gained focus");
});
window.addEventListener("blur", () => {
sendLogToServer("internal", "Page lost focus");
});
window.addEventListener('load', () => {
sendLogToServer("internal", "Page fully loaded");
});
window.addEventListener('DOMContentLoaded', () => {
sendLogToServer("internal", "DOM fully loaded and parsed");
});
window.addEventListener('online', () => {
sendLogToServer("internal", "Browser is online");
});
window.addEventListener('offline', () => {
sendLogToServer("warn", "Browser is offline");
});
window.addEventListener('resize', () => {
sendLogToServer("internal", `Window resized to ${window.innerWidth}x${window.innerHeight}px`);
});
window.addEventListener('orientationchange', () => {
sendLogToServer("internal", `Orientation changed to ${screen.orientation.type}`);
});
window.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
sendLogToServer("internal", "Entered fullscreen mode");
} else {
sendLogToServer("internal", "Exited fullscreen mode");
}
});
// url change event
window.addEventListener('hashchange', () => {
sendLogToServer("internal", `URL hash changed to ${location.hash}`);
});
window.addEventListener('popstate', () => {
sendLogToServer("internal", `History state changed: ${JSON.stringify(history.state)}`);
});
}
// #region copied from common/logger.js
/**
* Stringifies a log message, handling circular references and formatting.
* @param {any} log
* @param {Set<any>} [seen]
*/
function stringifyLog(log, seen = new Set(), depth = 0) {
const isServer = typeof window === "undefined";
const stringify_limits = {
string: isServer ? 100_000 : 2000,
object_keys: isServer ? 300 : 100,
object_depth: isServer ? 10 : 3,
array_items: isServer ? 2_000 : 100,
}
if (typeof log === "string") {
if (log.length > stringify_limits.string) log = `${log.slice(0, stringify_limits.string)}... <truncated ${log.length - stringify_limits.string} characters>`;
return log;
}
if (typeof log === "number" || typeof log === "boolean") {
return String(log);
}
if (log === null) {
return "null";
}
if (log === undefined) {
return "undefined";
}
if (typeof log === "function") {
return "<function>";
}
if (seen.has(log)) return "<circular>";
if (Array.isArray(log)) {
seen.add(log);
return stringifyArray(log);
}
if (typeof log === "object") {
if (depth > stringify_limits.object_depth) {
return "<object too deep>";
}
seen.add(log);
// const str = JSON.stringify(log, (key, value) => {
// if (typeof value === "function") return "<function>";
// if (typeof value === "string") return stringifyLog(value, seen, depth + 1);
// if (typeof value === "object") {
// if (seen.has(value)) return "<circular>";
// seen.add(value);
// }
// return value;
// });
// return str;
const keys = Object.keys(log);
let res = "{";
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
let value = log[key];
if (typeof value === "number") {
// clamp precision for numbers
value = Number(value.toFixed(6));
}
let str = stringifyLog(value, seen, depth + 1);
if (typeof value === "object") {
if (Array.isArray(value)) {
str = `[${str}]`;
}
}
else if (typeof value === "string") {
str = `"${str}"`;
}
if (i > 0) res += ", ";
res += `"${key}":${str}`;
}
res += "}";
return res;
// let entries = Object.entries(log).map(([key, value], index) => {
// if (index > stringify_limits.object_keys) return `"${key}": <truncated>`;
// return `"${key}": ${stringifyLog(value, seen, depth + 1)}`;
// });
// return `{ ${entries.join(", ")} }`;
}
return String(log);
function stringifyArray(arr) {
let res = "";
for (let i = 0; i < arr.length; i++) {
let entry = arr[i];
if (res && i > 0) res += ", ";
if (i > stringify_limits.array_items) {
res += "<truncated " + (arr.length - i) + ">";
break;
}
res += stringifyLog(entry, seen, depth + 1);
}
return res;
}
}