Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
393 lines
11 KiB
JavaScript
393 lines
11 KiB
JavaScript
/**
|
|
* Error Tracking Module
|
|
*
|
|
* Provides centralized error tracking and reporting.
|
|
* Features:
|
|
* - Error collection and grouping
|
|
* - Error reporting to backend
|
|
* - Error analytics
|
|
* - Integration with ErrorBoundary
|
|
* - Source map support
|
|
* - Error filtering and sampling
|
|
*/
|
|
|
|
import { Logger } from '../../core/logger.js';
|
|
|
|
/**
|
|
* ErrorTracker - Centralized error tracking
|
|
*/
|
|
export class ErrorTracker {
|
|
constructor(config = {}) {
|
|
this.config = {
|
|
endpoint: config.endpoint || '/api/errors',
|
|
enabled: config.enabled ?? true,
|
|
sampleRate: config.sampleRate ?? 1.0, // 0.0 to 1.0
|
|
maxErrors: config.maxErrors || 100,
|
|
groupingWindow: config.groupingWindow || 60000, // 1 minute
|
|
includeStack: config.includeStack ?? true,
|
|
includeContext: config.includeContext ?? true,
|
|
includeUserAgent: config.includeUserAgent ?? true,
|
|
includeUrl: config.includeUrl ?? true,
|
|
filters: config.filters || [],
|
|
beforeSend: config.beforeSend || null,
|
|
...config
|
|
};
|
|
|
|
this.errors = [];
|
|
this.errorGroups = new Map(); // Map<fingerprint, ErrorGroup>
|
|
this.reportQueue = [];
|
|
this.isReporting = false;
|
|
|
|
// Initialize error handlers
|
|
if (this.config.enabled) {
|
|
this.init();
|
|
}
|
|
|
|
Logger.info('[ErrorTracker] Initialized', {
|
|
enabled: this.config.enabled,
|
|
endpoint: this.config.endpoint,
|
|
sampleRate: this.config.sampleRate
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a new ErrorTracker instance
|
|
*/
|
|
static create(config = {}) {
|
|
return new ErrorTracker(config);
|
|
}
|
|
|
|
/**
|
|
* Initialize error tracking
|
|
*/
|
|
init() {
|
|
// Global error handler
|
|
window.addEventListener('error', (event) => {
|
|
this.captureException(event.error || new Error(event.message), {
|
|
type: 'unhandled',
|
|
filename: event.filename,
|
|
lineno: event.lineno,
|
|
colno: event.colno
|
|
});
|
|
});
|
|
|
|
// Unhandled promise rejection handler
|
|
window.addEventListener('unhandledrejection', (event) => {
|
|
this.captureException(event.reason, {
|
|
type: 'unhandledrejection'
|
|
});
|
|
});
|
|
|
|
// Report errors periodically
|
|
this.startReporting();
|
|
}
|
|
|
|
/**
|
|
* Capture an exception
|
|
*/
|
|
captureException(error, context = {}) {
|
|
if (!this.config.enabled) {
|
|
return;
|
|
}
|
|
|
|
// Sample rate check
|
|
if (Math.random() > this.config.sampleRate) {
|
|
Logger.debug('[ErrorTracker] Error sampled out');
|
|
return;
|
|
}
|
|
|
|
// Apply filters
|
|
if (this.shouldFilter(error, context)) {
|
|
Logger.debug('[ErrorTracker] Error filtered out');
|
|
return;
|
|
}
|
|
|
|
// Create error data
|
|
const errorData = this.createErrorData(error, context);
|
|
|
|
// Apply beforeSend hook
|
|
if (this.config.beforeSend) {
|
|
const modified = this.config.beforeSend(errorData);
|
|
if (modified === null || modified === false) {
|
|
return; // Blocked by beforeSend
|
|
}
|
|
if (modified) {
|
|
Object.assign(errorData, modified);
|
|
}
|
|
}
|
|
|
|
// Add to errors array
|
|
this.errors.push(errorData);
|
|
|
|
// Limit errors array size
|
|
if (this.errors.length > this.config.maxErrors) {
|
|
this.errors.shift();
|
|
}
|
|
|
|
// Group errors
|
|
this.groupError(errorData);
|
|
|
|
// Queue for reporting
|
|
this.reportQueue.push(errorData);
|
|
|
|
Logger.debug('[ErrorTracker] Error captured', errorData);
|
|
|
|
// Trigger error event
|
|
this.triggerErrorEvent(errorData);
|
|
}
|
|
|
|
/**
|
|
* Create error data object
|
|
*/
|
|
createErrorData(error, context = {}) {
|
|
const errorData = {
|
|
message: error?.message || String(error),
|
|
name: error?.name || 'Error',
|
|
stack: this.config.includeStack ? this.getStackTrace(error) : undefined,
|
|
timestamp: Date.now(),
|
|
type: context.type || 'error',
|
|
context: this.config.includeContext ? {
|
|
...context,
|
|
userAgent: this.config.includeUserAgent ? navigator.userAgent : undefined,
|
|
url: this.config.includeUrl ? window.location.href : undefined,
|
|
referrer: document.referrer || undefined,
|
|
viewport: {
|
|
width: window.innerWidth,
|
|
height: window.innerHeight
|
|
}
|
|
} : context
|
|
};
|
|
|
|
// Add additional error properties
|
|
if (error && typeof error === 'object') {
|
|
Object.keys(error).forEach(key => {
|
|
if (!['message', 'name', 'stack'].includes(key)) {
|
|
errorData[key] = error[key];
|
|
}
|
|
});
|
|
}
|
|
|
|
return errorData;
|
|
}
|
|
|
|
/**
|
|
* Get stack trace
|
|
*/
|
|
getStackTrace(error) {
|
|
if (error?.stack) {
|
|
return error.stack;
|
|
}
|
|
|
|
try {
|
|
throw new Error();
|
|
} catch (e) {
|
|
return e.stack || 'No stack trace available';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate error fingerprint for grouping
|
|
*/
|
|
generateFingerprint(errorData) {
|
|
// Group by message and stack trace (first few lines)
|
|
const message = errorData.message || '';
|
|
const stack = errorData.stack || '';
|
|
const stackLines = stack.split('\n').slice(0, 3).join('\n');
|
|
|
|
return `${errorData.name}:${message}:${stackLines}`;
|
|
}
|
|
|
|
/**
|
|
* Group errors
|
|
*/
|
|
groupError(errorData) {
|
|
const fingerprint = this.generateFingerprint(errorData);
|
|
const now = Date.now();
|
|
|
|
if (!this.errorGroups.has(fingerprint)) {
|
|
this.errorGroups.set(fingerprint, {
|
|
fingerprint,
|
|
count: 0,
|
|
firstSeen: now,
|
|
lastSeen: now,
|
|
errors: []
|
|
});
|
|
}
|
|
|
|
const group = this.errorGroups.get(fingerprint);
|
|
group.count++;
|
|
group.lastSeen = now;
|
|
group.errors.push(errorData);
|
|
|
|
// Limit errors in group
|
|
if (group.errors.length > 10) {
|
|
group.errors.shift();
|
|
}
|
|
|
|
// Clean up old groups
|
|
this.cleanupGroups(now);
|
|
}
|
|
|
|
/**
|
|
* Clean up old error groups
|
|
*/
|
|
cleanupGroups(now) {
|
|
const cutoff = now - this.config.groupingWindow;
|
|
|
|
for (const [fingerprint, group] of this.errorGroups.entries()) {
|
|
if (group.lastSeen < cutoff) {
|
|
this.errorGroups.delete(fingerprint);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if error should be filtered
|
|
*/
|
|
shouldFilter(error, context) {
|
|
for (const filter of this.config.filters) {
|
|
if (typeof filter === 'function') {
|
|
if (filter(error, context) === false) {
|
|
return true; // Filter out
|
|
}
|
|
} else if (filter instanceof RegExp) {
|
|
const message = error?.message || String(error);
|
|
if (filter.test(message)) {
|
|
return true; // Filter out
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Start reporting errors
|
|
*/
|
|
startReporting() {
|
|
// Report errors periodically
|
|
setInterval(() => {
|
|
this.flushReports();
|
|
}, 5000); // Every 5 seconds
|
|
}
|
|
|
|
/**
|
|
* Flush error reports to backend
|
|
*/
|
|
async flushReports() {
|
|
if (this.isReporting || this.reportQueue.length === 0) {
|
|
return;
|
|
}
|
|
|
|
this.isReporting = true;
|
|
|
|
try {
|
|
const errorsToReport = [...this.reportQueue];
|
|
this.reportQueue = [];
|
|
|
|
if (errorsToReport.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Send errors to backend
|
|
const response = await fetch(this.config.endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
body: JSON.stringify({
|
|
errors: errorsToReport,
|
|
errorGroups: Array.from(this.errorGroups.values()).map(group => ({
|
|
fingerprint: group.fingerprint,
|
|
count: group.count,
|
|
firstSeen: group.firstSeen,
|
|
lastSeen: group.lastSeen
|
|
}))
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Error reporting failed: ${response.status}`);
|
|
}
|
|
|
|
Logger.debug('[ErrorTracker] Errors reported', { count: errorsToReport.length });
|
|
} catch (error) {
|
|
Logger.error('[ErrorTracker] Failed to report errors', error);
|
|
// Re-queue errors for retry
|
|
// Note: In production, you might want to limit retries
|
|
} finally {
|
|
this.isReporting = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Manually report errors
|
|
*/
|
|
async report() {
|
|
await this.flushReports();
|
|
}
|
|
|
|
/**
|
|
* Get error groups
|
|
*/
|
|
getErrorGroups() {
|
|
return Array.from(this.errorGroups.values());
|
|
}
|
|
|
|
/**
|
|
* Get errors
|
|
*/
|
|
getErrors() {
|
|
return [...this.errors];
|
|
}
|
|
|
|
/**
|
|
* Clear errors
|
|
*/
|
|
clearErrors() {
|
|
this.errors = [];
|
|
this.errorGroups.clear();
|
|
this.reportQueue = [];
|
|
}
|
|
|
|
/**
|
|
* Trigger error event
|
|
*/
|
|
triggerErrorEvent(errorData) {
|
|
const event = new CustomEvent('error-tracker:error', {
|
|
detail: errorData,
|
|
bubbles: true
|
|
});
|
|
window.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Destroy error tracker
|
|
*/
|
|
destroy() {
|
|
// Flush remaining errors
|
|
this.flushReports();
|
|
|
|
// Clear data
|
|
this.clearErrors();
|
|
|
|
Logger.info('[ErrorTracker] Destroyed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a global error tracker instance
|
|
*/
|
|
let globalErrorTracker = null;
|
|
|
|
/**
|
|
* Get or create global error tracker
|
|
*/
|
|
export function getGlobalErrorTracker(config = {}) {
|
|
if (!globalErrorTracker) {
|
|
globalErrorTracker = ErrorTracker.create(config);
|
|
}
|
|
return globalErrorTracker;
|
|
}
|
|
|