Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"version": "1.0.0",
"description": "PR Readiness Checker - Track and monitor GitHub Pull Request status",
"scripts": {
"dev": "wrangler dev",
"fingerprint": "node scripts/fingerprint.js",
"dev": "npm run fingerprint && wrangler dev",
"deploy": "wrangler deploy",
"db:create": "wrangler d1 create pr_tracker",
"db:migrate:local": "wrangler d1 migrations apply pr_tracker --local",
Expand Down
5 changes: 5 additions & 0 deletions public/asset-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"error-reporter.js": "error-reporter.9455923c.js",
"how-it-works.js": "how-it-works.3ebd3397.js",
"theme.js": "theme.196e3e21.js"
}
10 changes: 5 additions & 5 deletions public/diagnostics.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
}
})();
</script>
<script src="error-reporter.js"></script>
<script src="error-reporter.9455923c.js"></script>

<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
Expand Down Expand Up @@ -55,10 +55,10 @@
class="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-100">
<i class="fab fa-github"></i> GitHub
</a>
<button id="themeToggle"
<button data-theme-toggle
class="rounded-lg p-2 text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-700 transition-colors"
title="Toggle dark/light mode">
<i class="fas fa-moon" id="themeIcon"></i>
<i class="fas fa-moon" data-theme-icon></i>
</button>
</nav>
</div>
Expand Down Expand Up @@ -168,8 +168,8 @@ <h1 class="text-3xl font-bold text-slate-900 dark:text-white mb-2 flex items-cen
buildList();

// Theme toggle
const themeToggle = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon');
const themeToggle = document.querySelector('[data-theme-toggle]');
const themeIcon = document.querySelector('[data-theme-icon]');
themeToggle.addEventListener('click', () => {
const isDark = document.documentElement.classList.toggle('dark');
themeIcon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
Expand Down
158 changes: 158 additions & 0 deletions public/error-reporter.9455923c.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Global error reporter: forwards JS errors to the backend,
// which relays them to the configured SLACK_ERROR_WEBHOOK.
(function () {
function safeTruncate(text, maxLen) {
var str = String(text || '');
if (str.length <= maxLen) return str;
return str.slice(0, Math.max(0, maxLen - 1)) + '…';
}

function redactSensitive(text) {
var str = String(text || '');
// Redact obvious key/value secrets and auth-like tokens.
str = str.replace(/(authorization|token|apikey|api[_-]?key|password|passwd|cookie|set-cookie|session|secret|bearer)\s*[:=]\s*([^\s,;]+)/gi, '$1:[redacted]');
// Redact email addresses.
str = str.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[redacted-email]');
// Redact long token-like strings.
str = str.replace(/\b[A-Za-z0-9_\-]{24,}\b/g, '[redacted-token]');
return str;
}

function sanitizeText(text, maxLen) {
return safeTruncate(redactSensitive(text), maxLen);
}

function sanitizeExtra(extra) {
var clean = {};
var source = extra || {};

Object.keys(source).forEach(function (key) {
var value = source[key];
if (value == null) {
clean[key] = value;
return;
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
clean[key] = sanitizeText(value, 500);
return;
}
// Avoid serializing arbitrary objects into error telemetry.
clean[key] = '[redacted-object]';
});

return clean;
}

function getPageUrl() {
try {
return location.origin + location.pathname;
} catch (e) {
return location.pathname || '';
}
}

function sendPayload(payload) {
try {
var body = JSON.stringify(payload);

// Prefer sendBeacon (good for unload), fallback to fetch if beacon fails.
var ok = false;
try {
ok = navigator.sendBeacon('/api/client-error', body);
} catch (e) {
ok = false;
}

if (!ok) {
fetch('/api/client-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body,
keepalive: true,
}).catch(function () { });
}
} catch (e) { /* ignore reporting failures */ }
}

function reportError(errorType, message, stack, extra) {
var payload = Object.assign(
{
error_type: sanitizeText(errorType || 'Error', 100),
message: sanitizeText(message || 'Unknown error', 300),
stack: sanitizeText(stack || '', 2000),
},
sanitizeExtra(extra)
);
sendPayload(payload);
}

// 1) Catch runtime errors + resource loading errors (capture=true is important for resources)
window.addEventListener('error', function (event) {
// Resource errors (script/img/link) often have event.target and no event.error
var target = event.target || {};
var resourceUrl = target.src || target.href;

var shouldReportResource =
target.tagName === 'SCRIPT' ||
target.tagName === 'LINK' ||
(target.tagName === 'IMG' && !target.hasAttribute('onerror') && !target.hasAttribute('data-ignore-error'));

if (resourceUrl && shouldReportResource) {
reportError(
'ResourceError',
'Failed to load or execute resource',
(event.error && event.error.stack) || '',
{ url: getPageUrl(), resource: resourceUrl }
);
return;
}

// Normal runtime error (ReferenceError/TypeError/etc.)
reportError(
(event.error && event.error.name) || 'Error',
event.message || (event.error && event.error.message) || String(event.error) || 'Unknown error',
(event.error && event.error.stack) || '',
{ url: getPageUrl(), line: event.lineno, col: event.colno }
);
}, true);

// 2) Catch unhandled promise rejections
window.addEventListener('unhandledrejection', function (event) {
var reason = event.reason || {};
reportError(
(reason.name) || 'UnhandledRejection',
reason.message || String(reason),
reason.stack || '',
{ url: getPageUrl() }
);
});

// 3) Forward handled errors that are logged via console.error
// This helps when "real errors" are caught by try/catch and only logged.
(function hookConsoleError() {
var original = console.error;
console.error = function () {
try {
var args = Array.prototype.slice.call(arguments);
var errors = args.filter(function (a) { return a instanceof Error; });

if (errors.length > 0) {
var primary = errors[0];
reportError(
primary.name || 'ConsoleError',
(primary.name || 'ConsoleError') + ': ' + (primary.message || ''),
primary.stack || '',
{
url: getPageUrl(),
source: 'console.error:unhandled',
report_channel: 'dedupe-candidate',
error_count: String(errors.length),
}
);
}
} catch (e) { /* ignore */ }

return original.apply(console, arguments);
};
})();
})();
103 changes: 81 additions & 22 deletions public/error-reporter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,56 @@
// Global error reporter: forwards JS errors to the backend,
// which relays them to the configured SLACK_ERROR_WEBHOOK.
(function () {
function safeTruncate(text, maxLen) {
var str = String(text || '');
if (str.length <= maxLen) return str;
return str.slice(0, Math.max(0, maxLen - 1)) + '…';
}

function redactSensitive(text) {
var str = String(text || '');
// Redact obvious key/value secrets and auth-like tokens.
str = str.replace(/(authorization|token|apikey|api[_-]?key|password|passwd|cookie|set-cookie|session|secret|bearer)\s*[:=]\s*([^\s,;]+)/gi, '$1:[redacted]');
// Redact email addresses.
str = str.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[redacted-email]');
// Redact long token-like strings.
str = str.replace(/\b[A-Za-z0-9_\-]{24,}\b/g, '[redacted-token]');
return str;
}

function sanitizeText(text, maxLen) {
return safeTruncate(redactSensitive(text), maxLen);
}

function sanitizeExtra(extra) {
var clean = {};
var source = extra || {};

Object.keys(source).forEach(function (key) {
var value = source[key];
if (value == null) {
clean[key] = value;
return;
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
clean[key] = sanitizeText(value, 500);
return;
}
// Avoid serializing arbitrary objects into error telemetry.
clean[key] = '[redacted-object]';
});

return clean;
}

function getPageUrl() {
try {
return location.origin + location.pathname;
} catch (e) {
return location.pathname || '';
}
}

function sendPayload(payload) {
try {
var body = JSON.stringify(payload);
Expand All @@ -26,8 +76,12 @@

function reportError(errorType, message, stack, extra) {
var payload = Object.assign(
{ error_type: errorType, message: message, stack: stack || '' },
extra || {}
{
error_type: sanitizeText(errorType || 'Error', 100),
message: sanitizeText(message || 'Unknown error', 300),
stack: sanitizeText(stack || '', 2000),
},
sanitizeExtra(extra)
);
sendPayload(payload);
}
Expand All @@ -38,12 +92,17 @@
var target = event.target || {};
var resourceUrl = target.src || target.href;

if (resourceUrl && (target.tagName === 'SCRIPT' || target.tagName === 'LINK' || target.tagName === 'IMG')) {
var shouldReportResource =
target.tagName === 'SCRIPT' ||
target.tagName === 'LINK' ||
(target.tagName === 'IMG' && !target.hasAttribute('onerror') && !target.hasAttribute('data-ignore-error'));

if (resourceUrl && shouldReportResource) {
reportError(
'ResourceError',
'Failed to load or execute resource',
(event.error && event.error.stack) || '',
{ url: location.href, resource: resourceUrl }
{ url: getPageUrl(), resource: resourceUrl }
);
return;
}
Expand All @@ -53,7 +112,7 @@
(event.error && event.error.name) || 'Error',
event.message || (event.error && event.error.message) || String(event.error) || 'Unknown error',
(event.error && event.error.stack) || '',
{ url: location.href, line: event.lineno, col: event.colno }
{ url: getPageUrl(), line: event.lineno, col: event.colno }
);
}, true);

Expand All @@ -64,7 +123,7 @@
(reason.name) || 'UnhandledRejection',
reason.message || String(reason),
reason.stack || '',
{ url: location.href }
{ url: getPageUrl() }
);
});

Expand All @@ -75,22 +134,22 @@
console.error = function () {
try {
var args = Array.prototype.slice.call(arguments);
var msg = args.map(function (a) {
if (a instanceof Error) {
return (a.name + ': ' + a.message + '\n' + (a.stack || ''));
}
if (typeof a === 'object') {
try { return JSON.stringify(a); } catch (_) { return '[object]'; }
}
return String(a);
}).join(' ');

reportError(
'ConsoleError',
msg.slice(0, 300),
msg.slice(0, 2000),
{ url: location.href, source: 'console.error' }
);
var errors = args.filter(function (a) { return a instanceof Error; });

if (errors.length > 0) {
var primary = errors[0];
reportError(
primary.name || 'ConsoleError',
(primary.name || 'ConsoleError') + ': ' + (primary.message || ''),
primary.stack || '',
{
url: getPageUrl(),
source: 'console.error:unhandled',
report_channel: 'dedupe-candidate',
error_count: String(errors.length),
}
);
}
} catch (e) { /* ignore */ }

return original.apply(console, arguments);
Expand Down
Loading
Loading