TenantAtlas/apps/platform/public/js/tenantpilot/unhandled-rejection-logger.js
2026-04-10 23:34:02 +02:00

174 lines
4.8 KiB
JavaScript

(() => {
if (typeof window === 'undefined') {
return;
}
if (window.__tenantpilotUnhandledRejectionLoggerApplied) {
return;
}
window.__tenantpilotUnhandledRejectionLoggerApplied = true;
const recentKeys = new Map();
const cleanupRecentKeys = (nowMs) => {
for (const [key, timestampMs] of recentKeys.entries()) {
if (nowMs - timestampMs > 5_000) {
recentKeys.delete(key);
}
}
};
const isTransportEnvelope = (value) => {
return value !== null
&& typeof value === 'object'
&& Object.prototype.hasOwnProperty.call(value, 'status')
&& Object.prototype.hasOwnProperty.call(value, 'body')
&& Object.prototype.hasOwnProperty.call(value, 'json')
&& Object.prototype.hasOwnProperty.call(value, 'errors');
};
const isCancellationReason = (reason) => {
if (!isTransportEnvelope(reason)) {
return false;
}
return reason.status === null
&& reason.body === null
&& reason.json === null
&& reason.errors === null;
};
const isPageHiddenOrInactive = () => {
if (document.visibilityState !== 'visible') {
return true;
}
return typeof document.hasFocus === 'function'
? document.hasFocus() === false
: false;
};
const isExpectedBackgroundTransportFailure = (reason) => {
if (isCancellationReason(reason)) {
return true;
}
if (!isTransportEnvelope(reason) || !isPageHiddenOrInactive()) {
return false;
}
return (reason.status === 419 && typeof reason.body === 'string' && reason.body.includes('Page Expired'))
|| (reason.status === 404 && typeof reason.body === 'string' && reason.body.includes('Not Found'));
};
const normalizeReason = (value, depth = 0) => {
if (depth > 3) {
return '[max-depth-reached]';
}
if (value instanceof Error) {
return {
type: 'Error',
name: value.name,
message: value.message,
stack: value.stack,
};
}
if (value === null || value === undefined) {
return value;
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (Array.isArray(value)) {
return value.slice(0, 10).map((item) => normalizeReason(item, depth + 1));
}
if (typeof value === 'object') {
const result = {};
const allowedKeys = [
'message',
'stack',
'name',
'type',
'status',
'body',
'json',
'errors',
'reason',
'code',
];
for (const key of allowedKeys) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
result[key] = normalizeReason(value[key], depth + 1);
}
}
if (Object.keys(result).length > 0) {
return result;
}
const stringTag = Object.prototype.toString.call(value);
return {
type: stringTag,
value: String(value),
};
}
return String(value);
};
const toStableJson = (payload) => {
try {
return JSON.stringify(payload);
} catch {
return JSON.stringify({
source: payload.source,
href: payload.href,
timestamp: payload.timestamp,
reason: '[unserializable]',
});
}
};
window.addEventListener('unhandledrejection', (event) => {
const normalizedReason = normalizeReason(event.reason);
const payload = {
source: 'window.unhandledrejection',
href: window.location.href,
timestamp: new Date().toISOString(),
reason: normalizedReason,
};
if (isExpectedBackgroundTransportFailure(normalizedReason)) {
event.preventDefault();
return;
}
const dedupeKey = toStableJson({
source: payload.source,
href: payload.href,
reason: payload.reason,
});
const payloadJson = toStableJson(payload);
const nowMs = Date.now();
cleanupRecentKeys(nowMs);
if (recentKeys.has(dedupeKey)) {
return;
}
recentKeys.set(dedupeKey, nowMs);
console.error(`TenantPilot unhandled promise rejection ${payloadJson}`);
});
})();