UNPKG

gt-error-tracker

Version:

GT Error Tracker SDK - Self-hosted error tracking that you own forever

490 lines (431 loc) 13.1 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); /** * GT Error Tracker SDK * Self-hosted error tracking that you own forever * @version 1.0.0 */ class GTErrorTracker { constructor() { this.config = { apiKey: null, projectId: null, apiUrl: null, environment: 'production', enabled: true, autoCapture: true, breadcrumbsEnabled: true, maxBreadcrumbs: 50 }; this.breadcrumbs = []; this.userContext = {}; this.tags = {}; this.customContext = {}; this.performanceMetrics = {}; this.isInitialized = false; } /** * Initialize GT Error Tracker * @param {Object} options Configuration options * @param {string} options.apiKey Your GT Error Tracker API key * @param {string} options.projectId Your project ID (optional) * @param {string} options.apiUrl API endpoint URL (default: https://api.gideonstechnology.com) * @param {string} options.environment Environment name (default: production) * @param {boolean} options.autoCapture Auto-capture window errors (default: true) */ init(options) { if (!options.apiKey) { console.error('[GT Error Tracker] API key is required'); return; } this.config = { ...this.config, ...options, apiUrl: options.apiUrl || 'https://api.gideonstechnology.com' }; this.isInitialized = true; // Auto-capture window errors if (this.config.autoCapture && typeof window !== 'undefined') { this._setupGlobalHandlers(); } console.log('[GT Error Tracker] Initialized successfully'); } /** * Setup global error handlers * @private */ _setupGlobalHandlers() { // Handle uncaught errors window.addEventListener('error', (event) => { this.captureException(event.error || new Error(event.message), { extra: { filename: event.filename, lineno: event.lineno, colno: event.colno } }); }); // Handle unhandled promise rejections window.addEventListener('unhandledrejection', (event) => { this.captureException(event.reason, { extra: { type: 'unhandled_rejection' } }); }); // Capture console errors const originalError = console.error; console.error = (...args) => { this.captureMessage(args.join(' '), 'error'); originalError.apply(console, args); }; } /** * Capture an exception * @param {Error} error Error object * @param {Object} options Additional options * @param {Object} options.tags Custom tags * @param {Object} options.extra Extra context * @param {Object} options.user User information * @param {Object} options.request HTTP request details * @param {Object} options.response HTTP response details */ captureException(error, options = {}) { if (!this.config.enabled || !this.isInitialized) { return null; } const parsedStack = this._parseStackTrace(error.stack); const errorData = { message: error.message || String(error), stack: error.stack || new Error().stack, parsedStack, level: 'error', errorName: error.name, errorCode: error.code, url: typeof window !== 'undefined' ? window.location.href : options.url, userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : options.userAgent, timestamp: new Date().toISOString(), environment: this.config.environment, tags: { ...this.tags, ...options.tags }, context: { ...this.customContext, ...options.extra, errorType: error.name, browser: this._getBrowserInfo(), performance: this.performanceMetrics }, request: this._captureRequest(options.request), response: this._captureResponse(options.response), breadcrumbs: this.breadcrumbs.slice(-this.config.maxBreadcrumbs), user: { id: this.userContext.id, email: this.userContext.email, username: this.userContext.username, ...options.user } }; return this._sendError(errorData); } /** * Capture a message * @param {string} message Message to log * @param {string} level Level (info, warning, error) * @param {Object} options Additional options */ captureMessage(message, level = 'info', options = {}) { if (!this.config.enabled || !this.isInitialized) { return null; } const errorData = { message, level, url: typeof window !== 'undefined' ? window.location.href : options.url, userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : options.userAgent, timestamp: new Date().toISOString(), environment: this.config.environment, tags: { ...this.tags, ...options.tags }, context: { ...this.customContext, ...options.extra, performance: this.performanceMetrics }, request: this._captureRequest(options.request), response: this._captureResponse(options.response), breadcrumbs: this.breadcrumbs.slice(-this.config.maxBreadcrumbs), user: { id: this.userContext.id, email: this.userContext.email, username: this.userContext.username, ...options.user } }; return this._sendError(errorData); } /** * Add a breadcrumb * @param {Object} breadcrumb Breadcrumb data * @param {string} breadcrumb.message Breadcrumb message * @param {string} breadcrumb.category Category (navigation, click, http, etc.) * @param {string} breadcrumb.level Level (info, warning, error) * @param {Object} breadcrumb.data Additional data */ addBreadcrumb(breadcrumb) { if (!this.config.breadcrumbsEnabled) { return; } this.breadcrumbs.push({ timestamp: new Date().toISOString(), message: breadcrumb.message || '', category: breadcrumb.category || 'manual', level: breadcrumb.level || 'info', data: breadcrumb.data || {} }); // Keep only last N breadcrumbs if (this.breadcrumbs.length > this.config.maxBreadcrumbs) { this.breadcrumbs.shift(); } } /** * Set user context * @param {Object} user User information * @param {string} user.id User ID * @param {string} user.email User email * @param {string} user.username Username */ setUser(user) { this.userContext = { id: user.id, email: user.email, username: user.username, ...user }; } /** * Set custom tags * @param {Object} tags Tags to add */ setTags(tags) { this.tags = { ...this.tags, ...tags }; } /** * Set a single tag * @param {string} key Tag key * @param {string} value Tag value */ setTag(key, value) { this.tags[key] = value; } /** * Set custom context data * @param {Object} context Context data */ setContext(context) { this.customContext = { ...this.customContext, ...context }; } /** * Clear custom context */ clearContext() { this.customContext = {}; } /** * Track performance metric * @param {string} name Metric name * @param {number} value Metric value * @param {string} unit Unit of measurement */ trackPerformance(name, value, unit = 'ms') { this.performanceMetrics[name] = { value, unit, timestamp: new Date().toISOString() }; } /** * Capture HTTP request details * @param {Object} request Request object * @returns {Object} Sanitized request data * @private */ _captureRequest(request) { if (!request) return undefined; return { method: request.method, url: request.url, headers: this._sanitizeHeaders(request.headers), query: request.query, body: this._sanitizeBody(request.body), ip: request.ip, cookies: request.cookies ? Object.keys(request.cookies) : undefined }; } /** * Capture HTTP response details * @param {Object} response Response object * @returns {Object} Response data * @private */ _captureResponse(response) { if (!response) return undefined; return { statusCode: response.statusCode || response.status, statusMessage: response.statusMessage, headers: this._sanitizeHeaders(response.headers), body: this._sanitizeBody(response.body), responseTime: response.responseTime }; } /** * Sanitize headers by removing sensitive data * @param {Object} headers Headers object * @returns {Object} Sanitized headers * @private */ _sanitizeHeaders(headers) { if (!headers) return undefined; const sanitized = { ...headers }; const sensitiveKeys = ['authorization', 'cookie', 'x-api-key', 'x-auth-token']; sensitiveKeys.forEach(key => { if (sanitized[key]) { sanitized[key] = '[REDACTED]'; } }); return sanitized; } /** * Sanitize request/response body * @param {any} body Body data * @returns {any} Sanitized body * @private */ _sanitizeBody(body) { if (!body) return undefined; if (typeof body === 'string' && body.length > 1000) { return body.substring(0, 1000) + '... [truncated]'; } if (typeof body === 'object') { const sanitized = { ...body }; const sensitiveKeys = ['password', 'token', 'secret', 'apiKey', 'creditCard']; sensitiveKeys.forEach(key => { if (sanitized[key]) { sanitized[key] = '[REDACTED]'; } }); return sanitized; } return body; } /** * Parse stack trace into structured format * @param {string} stack Stack trace string * @returns {Array} Parsed stack frames * @private */ _parseStackTrace(stack) { if (!stack) return []; const frames = []; const lines = stack.split('\n'); for (const line of lines) { // Match various stack trace formats const match = line.match(/at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/); if (match) { frames.push({ function: match[1] || 'anonymous', filename: match[2], lineno: parseInt(match[3]), colno: parseInt(match[4]) }); } } return frames; } /** * Send error to GT Error Tracker API * @param {Object} errorData Error data * @returns {Promise} Promise that resolves when error is sent * @private */ async _sendError(errorData) { try { const response = await fetch(`${this.config.apiUrl}/api/error-tracker/v1/errors`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': this.config.apiKey }, body: JSON.stringify(errorData) }); if (!response.ok) { console.error('[GT Error Tracker] Failed to send error:', response.statusText); return null; } const result = await response.json(); return result.errorId; } catch (error) { console.error('[GT Error Tracker] Network error:', error); return null; } } /** * Get browser information * @returns {Object} Browser info * @private */ _getBrowserInfo() { if (typeof window === 'undefined') { return { type: 'server' }; } return { name: this._getBrowserName(), version: this._getBrowserVersion(), screenSize: `${window.screen.width}x${window.screen.height}`, viewport: `${window.innerWidth}x${window.innerHeight}` }; } /** * Get browser name * @returns {string} Browser name * @private */ _getBrowserName() { const userAgent = navigator.userAgent; if (userAgent.indexOf('Firefox') > -1) return 'Firefox'; if (userAgent.indexOf('Chrome') > -1) return 'Chrome'; if (userAgent.indexOf('Safari') > -1) return 'Safari'; if (userAgent.indexOf('Edge') > -1) return 'Edge'; if (userAgent.indexOf('MSIE') > -1 || userAgent.indexOf('Trident') > -1) return 'IE'; return 'Unknown'; } /** * Get browser version * @returns {string} Browser version * @private */ _getBrowserVersion() { const userAgent = navigator.userAgent; const match = userAgent.match(/(firefox|chrome|safari|edge|msie|trident(?=\/))\/?\s*(\d+)/i) || []; return match[2] || 'Unknown'; } /** * Enable/disable error tracking * @param {boolean} enabled Whether tracking is enabled */ setEnabled(enabled) { this.config.enabled = enabled; } /** * Check if tracking is enabled * @returns {boolean} Whether tracking is enabled */ isEnabled() { return this.config.enabled && this.isInitialized; } } // Create singleton instance const GTTracker = new GTErrorTracker(); // Export for different module systems if (typeof module !== 'undefined' && module.exports) { module.exports = GTTracker; module.exports.GTErrorTracker = GTErrorTracker; } if (typeof window !== 'undefined') { window.GTErrorTracker = GTTracker; } exports.GTErrorTracker = GTErrorTracker; exports.default = GTTracker;