TenantAtlas/apps/platform/public/js/tenantpilot/unhandled-rejection-logger.js
ahmido 9f6985291e feat: implement spec 192 record page header discipline (#226)
## Summary
- implement Spec 192 across the targeted Filament record, detail, and edit pages with explicit action-surface inventory and guard coverage
- add the focused Spec 192 browser smoke, feature tests, and spec artifacts under `specs/192-record-header-discipline`
- improve unhandled promise rejection diagnostics by correlating 419s to the underlying Livewire request URL
- disable panel-wide database notification polling on the admin, tenant, and system panels and cover the mitigation with focused tests

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/DatabaseNotificationsPollingTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/DatabaseNotificationsPollingTest.php tests/Feature/Filament/UnhandledRejectionLoggerAssetTest.php tests/Feature/Filament/FilamentNotificationsAssetsTest.php tests/Feature/Workspaces/ManagedTenantsLivewireUpdateTest.php tests/Feature/Filament/AdminSmokeTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- manual integrated-browser verification of the Spec 192 surfaces and the notification-polling mitigation

## Notes
- Livewire v4 / Filament v5 compliance remains unchanged.
- Provider registration stays in `bootstrap/providers.php`.
- No Global Search behavior was expanded.
- No destructive action confirmation semantics were relaxed.
- The full test suite was not run in this PR.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #226
2026-04-11 21:20:41 +00:00

392 lines
13 KiB
JavaScript

(() => {
if (typeof window === 'undefined') {
return;
}
if (window.__tenantpilotUnhandledRejectionLoggerApplied) {
return;
}
window.__tenantpilotUnhandledRejectionLoggerApplied = true;
const recentKeys = new Map();
const recentTransportFailures = [];
const cleanupRecentKeys = (nowMs) => {
for (const [key, timestampMs] of recentKeys.entries()) {
if (nowMs - timestampMs > 5_000) {
recentKeys.delete(key);
}
}
};
const cleanupRecentTransportFailures = (nowMs) => {
while (recentTransportFailures.length > 0 && nowMs - recentTransportFailures[0].timestampMs > 15_000) {
recentTransportFailures.shift();
}
};
const normalizeUrl = (value) => {
if (typeof value !== 'string' || value === '') {
return null;
}
try {
return new URL(value, window.location.href).href;
} catch {
return value;
}
};
const toBodySnippet = (body) => {
if (typeof body !== 'string' || body === '') {
return null;
}
return body.slice(0, 1_000);
};
const recordTransportFailure = ({ requestUrl, method, status, body, transportType }) => {
const nowMs = Date.now();
cleanupRecentTransportFailures(nowMs);
recentTransportFailures.push({
requestUrl: normalizeUrl(requestUrl),
method: typeof method === 'string' && method !== '' ? method.toUpperCase() : 'GET',
status: Number.isFinite(status) ? status : null,
bodySnippet: toBodySnippet(body),
transportType: typeof transportType === 'string' && transportType !== '' ? transportType : 'unknown',
timestampMs: nowMs,
});
if (recentTransportFailures.length > 30) {
recentTransportFailures.shift();
}
};
const resolveTransportMetadata = (reason) => {
if (reason === null || typeof reason !== 'object') {
return null;
}
const directRequestUrl = typeof reason.requestUrl === 'string'
? normalizeUrl(reason.requestUrl)
: (typeof reason.url === 'string' ? normalizeUrl(reason.url) : null);
if (directRequestUrl) {
return {
requestUrl: directRequestUrl,
method: typeof reason.method === 'string' ? reason.method.toUpperCase() : null,
transportType: 'reason',
};
}
if (!isTransportEnvelope(reason)) {
return null;
}
const nowMs = Date.now();
const reasonBodySnippet = toBodySnippet(reason.body);
cleanupRecentTransportFailures(nowMs);
for (let index = recentTransportFailures.length - 1; index >= 0; index -= 1) {
const candidate = recentTransportFailures[index];
if (nowMs - candidate.timestampMs > 5_000) {
break;
}
if (candidate.status !== reason.status) {
continue;
}
if (reasonBodySnippet !== null && candidate.bodySnippet !== null && candidate.bodySnippet !== reasonBodySnippet) {
continue;
}
return {
requestUrl: candidate.requestUrl,
method: candidate.method,
transportType: candidate.transportType,
};
}
return null;
};
const extractRequestMetadata = (input, init) => {
if (input instanceof Request) {
return {
requestUrl: normalizeUrl(input.url),
method: typeof input.method === 'string' && input.method !== ''
? input.method.toUpperCase()
: 'GET',
};
}
return {
requestUrl: normalizeUrl(typeof input === 'string' ? input : String(input ?? '')),
method: typeof init?.method === 'string' && init.method !== ''
? init.method.toUpperCase()
: 'GET',
};
};
if (typeof window.fetch === 'function' && !window.__tenantpilotUnhandledRejectionFetchInstrumented) {
const originalFetch = window.fetch.bind(window);
window.fetch = async (...args) => {
const [input, init] = args;
const transport = extractRequestMetadata(input, init);
try {
const response = await originalFetch(...args);
if (!response.ok) {
const clonedResponse = typeof response.clone === 'function' ? response.clone() : null;
if (clonedResponse && typeof clonedResponse.text === 'function') {
clonedResponse.text()
.then((body) => {
recordTransportFailure({
requestUrl: transport.requestUrl,
method: transport.method,
status: response.status,
body,
transportType: 'fetch',
});
})
.catch(() => {
recordTransportFailure({
requestUrl: transport.requestUrl,
method: transport.method,
status: response.status,
body: null,
transportType: 'fetch',
});
});
} else {
recordTransportFailure({
requestUrl: transport.requestUrl,
method: transport.method,
status: response.status,
body: null,
transportType: 'fetch',
});
}
}
return response;
} catch (error) {
recordTransportFailure({
requestUrl: transport.requestUrl,
method: transport.method,
status: null,
body: error instanceof Error ? error.message : String(error ?? ''),
transportType: 'fetch',
});
throw error;
}
};
window.__tenantpilotUnhandledRejectionFetchInstrumented = true;
}
if (typeof XMLHttpRequest !== 'undefined' && !window.__tenantpilotUnhandledRejectionXhrInstrumented) {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
this.__tenantpilotRequestMethod = typeof method === 'string' && method !== '' ? method.toUpperCase() : 'GET';
this.__tenantpilotRequestUrl = normalizeUrl(typeof url === 'string' ? url : String(url ?? ''));
return originalOpen.call(this, method, url, ...rest);
};
XMLHttpRequest.prototype.send = function (...args) {
if (!this.__tenantpilotTransportFailureListenerApplied) {
this.addEventListener('loadend', () => {
if (typeof this.status === 'number' && this.status >= 400) {
recordTransportFailure({
requestUrl: this.__tenantpilotRequestUrl,
method: this.__tenantpilotRequestMethod,
status: this.status,
body: typeof this.responseText === 'string' ? this.responseText : null,
transportType: 'xhr',
});
}
});
this.__tenantpilotTransportFailureListenerApplied = true;
}
return originalSend.apply(this, args);
};
window.__tenantpilotUnhandledRejectionXhrInstrumented = true;
}
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',
'url',
'requestUrl',
'method',
];
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 transport = resolveTransportMetadata(normalizedReason);
const payload = {
source: 'window.unhandledrejection',
href: window.location.href,
timestamp: new Date().toISOString(),
requestUrl: transport?.requestUrl ?? null,
requestMethod: transport?.method ?? null,
transportType: transport?.transportType ?? null,
reason: normalizedReason,
};
if (isExpectedBackgroundTransportFailure(normalizedReason)) {
event.preventDefault();
return;
}
const dedupeKey = toStableJson({
source: payload.source,
href: payload.href,
requestUrl: payload.requestUrl,
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}`);
});
})();