gt-error-tracker
Version:
GT Error Tracker SDK - Self-hosted error tracking that you own forever
490 lines (431 loc) • 13.1 kB
JavaScript
'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;