Update public/_kleap/kleap-nextjs-error-detector.js

This commit is contained in:
kleap-admin 2026-01-15 14:28:07 +00:00
parent bb4056a7f7
commit 88231e9313
1 changed files with 486 additions and 0 deletions

View File

@ -0,0 +1,486 @@
/**
* Kleap Next.js Error Detector
*
* Detects and reports Next.js build/runtime errors to parent window via postMessage
* Works with Next.js 15, 16, and Turbopack error overlay systems
*
* Version: 2.0.0
*/
(function () {
console.debug("[Kleap] Next.js error detector loaded v2.0.0");
// Only run inside iframe
const isInsideIframe = window.parent !== window;
if (!isInsideIframe) {
console.debug("[Kleap] Not inside iframe, skipping error detection");
return;
}
const PARENT_TARGET_ORIGIN = "*";
// Track reported errors to avoid duplicates
const reportedErrors = new Set();
const ERROR_COOLDOWN_MS = 5000;
/**
* Send error report to parent window
*/
function reportError(errorData) {
// Ensure we have valid error data
if (!errorData || typeof errorData !== "object") {
console.debug("[Kleap] Invalid error data, skipping report");
return;
}
// Ensure required fields exist
if (!errorData.type) errorData.type = "build-error";
if (!errorData.message) errorData.message = "Unknown error";
const errorKey = `${errorData.type}-${errorData.message}`;
// Check if already reported recently
if (reportedErrors.has(errorKey)) {
console.debug("[Kleap] Error already reported, skipping:", errorKey);
return;
}
// Mark as reported
reportedErrors.add(errorKey);
setTimeout(() => reportedErrors.delete(errorKey), ERROR_COOLDOWN_MS);
console.log("[Kleap] Reporting error to parent:", errorData);
window.parent.postMessage(
{
type: "nextjs-build-error",
payload: errorData,
},
PARENT_TARGET_ORIGIN,
);
}
/**
* Extract error details from Next.js error overlay
* Supports Next.js 15, 16, and Turbopack
*/
function extractNextJsError(container) {
try {
console.debug(
"[Kleap] Extracting error from:",
container.tagName,
container.id,
container.className,
);
// Method 1: Next.js 15 - Check for data-nextjs-dialog attributes
const errorTitle = container.querySelector("[data-nextjs-dialog-header]");
const errorBody = container.querySelector("[data-nextjs-dialog-body]");
if (errorTitle || errorBody) {
const title = errorTitle?.textContent?.trim() || "";
const body = errorBody?.textContent?.trim() || "";
return {
type: "build-error",
message: title || "Build error detected",
details: body,
fullText: `${title}\n\n${body}`.trim(),
source: "nextjs-dialog",
};
}
// Method 2: Next.js 16 / Turbopack - Look for shadow DOM or portal content
// Next.js 16 uses nextjs-portal custom element with shadow DOM
if (
container.tagName === "NEXTJS-PORTAL" ||
container.id?.includes("nextjs-portal")
) {
const shadowRoot = container.shadowRoot;
if (shadowRoot) {
const errorText = shadowRoot.textContent?.trim();
if (errorText && errorText.length > 10) {
const lines = errorText
.split("\n")
.map((l) => l.trim())
.filter(Boolean);
return {
type: "build-error",
message: lines[0]?.substring(0, 200) || "Build error detected",
details: errorText.substring(0, 1000),
fullText: errorText.substring(0, 2000),
source: "nextjs-portal-shadow",
};
}
}
}
// Method 3: Look for error message in generic containers (h1, h2, h4, pre)
const headings = container.querySelectorAll("h1, h2, h4");
const pre = container.querySelector("pre");
const code = container.querySelector("code");
if (headings.length > 0 || pre || code) {
let title = "";
for (const h of headings) {
const text = h?.textContent?.trim();
if (text && text.length > 5) {
title = text;
break;
}
}
const details =
pre?.textContent?.trim() || code?.textContent?.trim() || "";
if (title || details) {
return {
type: "build-error",
message: title || "Build error",
details: details.substring(0, 1000),
fullText:
container.textContent?.trim()?.substring(0, 2000) ||
"Unknown error",
source: "generic-heading",
};
}
}
// Method 4: Generic text extraction - check for error keywords
const text = container.textContent?.trim();
if (text && text.length > 10) {
// Check for common Next.js/React error patterns
const errorPatterns = [
"Module not found",
"Can't resolve",
"Build Error",
"Compile Error",
"Runtime Error",
"Unhandled Runtime Error",
"Error:",
"SyntaxError",
"TypeError",
"ReferenceError",
"Failed to compile",
"Import trace",
"Caused by:",
"at (",
];
const hasErrorPattern = errorPatterns.some((pattern) =>
text.includes(pattern),
);
if (hasErrorPattern) {
// Extract first meaningful line as message
const lines = text
.split("\n")
.map((l) => l.trim())
.filter(Boolean);
let message = "Build error detected";
// Find the most relevant line (usually contains "Error")
for (const line of lines) {
if (
line.includes("Error") ||
line.includes("Module not found") ||
line.includes("Can't resolve")
) {
message = line.substring(0, 200);
break;
}
}
return {
type: "build-error",
message: message,
details: text.substring(0, 1000),
fullText: text.substring(0, 2000),
source: "text-pattern",
};
}
}
// Method 5: Fallback - if it's an error overlay but we can't extract details
// Still report something so the parent knows an error occurred
if (
container.id?.includes("error") ||
container.className?.includes?.("error") ||
container.getAttribute?.("data-nextjs-dev-overlay") !== null
) {
const rawText = container.textContent?.trim();
if (rawText && rawText.length > 5) {
return {
type: "build-error",
message: rawText.substring(0, 200) || "Error overlay detected",
details: rawText.substring(0, 1000),
fullText: rawText.substring(0, 2000),
source: "fallback",
};
}
}
console.debug("[Kleap] Could not extract error details from container");
return null;
} catch (error) {
console.error("[Kleap] Error extracting Next.js error:", error);
return null;
}
}
/**
* Watch for Next.js error overlay in DOM
*/
function watchForNextJsErrors() {
console.debug("[Kleap] Setting up Next.js error detection");
// Configuration for MutationObserver
const config = {
childList: true,
subtree: true, // Watch all descendants (Next.js uses portals)
attributes: true,
attributeFilter: [
"data-nextjs-dev-overlay",
"data-nextjs-dialog",
"data-nextjs-toast",
],
};
// Callback when DOM changes
const observerCallback = function (mutationsList) {
for (const mutation of mutationsList) {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue;
// Check for Next.js error overlay elements
// Supports Next.js 15, 16, and Turbopack
const isErrorOverlay =
node.id?.includes("nextjs") ||
node.id?.includes("error") ||
node.tagName === "NEXTJS-PORTAL" ||
node.getAttribute?.("data-nextjs-dialog") !== null ||
node.getAttribute?.("data-nextjs-toast") !== null ||
node.getAttribute?.("data-nextjs-dev-overlay") !== null ||
node.className?.includes?.("nextjs") ||
node.className?.includes?.("error-overlay") ||
node.className?.includes?.("dev-overlay");
if (isErrorOverlay) {
console.log(
"[Kleap] Detected Next.js error overlay:",
node.tagName,
node.id,
);
// Wait a bit for content to populate (especially for shadow DOM)
setTimeout(() => {
const errorData = extractNextJsError(node);
if (errorData) {
reportError(errorData);
}
}, 200); // Increased timeout for shadow DOM content
}
// Also check children recursively
if (node.querySelector) {
const selectors = [
"[data-nextjs-dialog]",
"[data-nextjs-toast]",
"[data-nextjs-dev-overlay]",
"nextjs-portal",
'[id*="nextjs"]',
'[id*="error"]',
'[class*="error-overlay"]',
];
const errorChildren = node.querySelectorAll(selectors.join(", "));
errorChildren.forEach((child) => {
setTimeout(() => {
const errorData = extractNextJsError(child);
if (errorData) {
reportError(errorData);
}
}, 200);
});
}
}
}
// Also check for attribute changes (overlay appearing)
if (mutation.type === "attributes") {
const node = mutation.target;
if (node.nodeType === Node.ELEMENT_NODE) {
const errorData = extractNextJsError(node);
if (errorData) {
reportError(errorData);
}
}
}
}
};
// Start observing when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
console.debug("[Kleap] DOM loaded, starting error detection");
const observer = new MutationObserver(observerCallback);
observer.observe(document.body || document.documentElement, config);
// Check for existing errors
checkExistingErrors();
});
} else {
console.debug("[Kleap] DOM already loaded, starting error detection");
const observer = new MutationObserver(observerCallback);
observer.observe(document.body || document.documentElement, config);
// Check for existing errors
checkExistingErrors();
}
}
/**
* Check for errors that already exist in the DOM
*/
function checkExistingErrors() {
console.debug("[Kleap] Checking for existing errors");
try {
// Look for Next.js error elements (supports 15, 16, and Turbopack)
const selectors = [
"[data-nextjs-dialog]",
"[data-nextjs-toast]",
"[data-nextjs-dev-overlay]",
"nextjs-portal",
'[id*="nextjs"][id*="error"]',
'[id*="__next_error"]',
'div[class*="error-overlay"]',
'div[class*="dev-overlay"]',
];
selectors.forEach((selector) => {
try {
const elements = document.querySelectorAll(selector);
elements.forEach((element) => {
console.log(
"[Kleap] Found existing error element:",
element.tagName,
element.id,
);
const errorData = extractNextJsError(element);
if (errorData) {
reportError(errorData);
}
});
} catch {
// Ignore invalid selector errors
}
});
} catch (error) {
console.error("[Kleap] Error checking existing errors:", error);
}
}
/**
* Also intercept console.error for build errors
* Next.js sometimes logs errors to console before showing overlay
*/
const originalConsoleError = console.error;
console.error = function (...args) {
// Call original
originalConsoleError.apply(console, args);
// Check if it looks like a Next.js error
const message = args
.map((arg) => {
if (typeof arg === "string") return arg;
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
try {
return String(arg);
} catch {
return "[Object]";
}
})
.join(" ");
// Extended error patterns for Next.js 16 and Turbopack
const errorPatterns = [
"Module not found",
"Can't resolve",
"Build Error",
"Compile Error",
"Failed to compile",
"Turbopack build error",
"Error: ",
"SyntaxError:",
"TypeError:",
"ReferenceError:",
"ModuleNotFoundError",
"import trace for requested module",
];
const hasErrorPattern = errorPatterns.some((pattern) =>
message.toLowerCase().includes(pattern.toLowerCase()),
);
if (hasErrorPattern) {
console.log("[Kleap] Detected build error in console.error");
reportError({
type: "build-error",
message: message.substring(0, 200),
details: message.substring(0, 1000),
fullText: message.substring(0, 2000),
source: "console.error",
});
}
};
/**
* Also listen for unhandled errors and rejections
*/
window.addEventListener("error", function (event) {
const error = event.error;
if (error && error.message) {
const message = error.message;
if (
message.includes("Module not found") ||
message.includes("Can't resolve") ||
message.includes("Failed to compile")
) {
reportError({
type: "runtime-error",
message: message.substring(0, 200),
details: error.stack?.substring(0, 1000) || message,
fullText: error.stack?.substring(0, 2000) || message,
source: "window.error",
});
}
}
});
window.addEventListener("unhandledrejection", function (event) {
const reason = event.reason;
if (reason && (reason.message || typeof reason === "string")) {
const message = reason.message || String(reason);
if (
message.includes("Module not found") ||
message.includes("Can't resolve") ||
message.includes("Failed to compile")
) {
reportError({
type: "runtime-error",
message: message.substring(0, 200),
details: reason.stack?.substring(0, 1000) || message,
fullText: reason.stack?.substring(0, 2000) || message,
source: "unhandledrejection",
});
}
}
});
// Start watching
watchForNextJsErrors();
console.debug("[Kleap] Next.js error detector initialized");
})();