Don’t just be a tool user! Let’s build a frontend error monitoring SDK from scratch. This hands-on guide will walk you through creating your own error tracking system.

Why Build Your Own SDK?

While there are excellent error monitoring services like Sentry, Rollbar, and Bugsnag, building your own SDK helps you:

  • Understand how error monitoring works under the hood
  • Customize error tracking to your specific needs
  • Learn valuable debugging and monitoring concepts
  • Reduce dependency on third-party services

Core Features

Our SDK will include:

  1. Error Capture: Catch JavaScript errors, unhandled promise rejections
  2. Error Reporting: Send errors to a backend service
  3. User Context: Capture user information and session data
  4. Performance Monitoring: Track page load times and performance metrics
  5. Source Maps: Support for source map debugging

Implementation

1. Basic Error Capture

class ErrorMonitor {
  constructor(options = {}) {
    this.apiUrl = options.apiUrl || '/api/errors';
    this.environment = options.environment || 'production';
    this.init();
  }

  init() {
    // Capture unhandled errors
    window.addEventListener('error', (event) => {
      this.captureError({
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
        stack: event.error?.stack,
        type: 'javascript'
      });
    });

    // Capture unhandled promise rejections
    window.addEventListener('unhandledrejection', (event) => {
      this.captureError({
        message: event.reason?.message || 'Unhandled Promise Rejection',
        stack: event.reason?.stack,
        type: 'promise'
      });
    });
  }

  captureError(errorData) {
    const errorReport = {
      ...errorData,
      timestamp: new Date().toISOString(),
      url: window.location.href,
      userAgent: navigator.userAgent,
      environment: this.environment,
      userId: this.getUserId(),
      sessionId: this.getSessionId()
    };

    this.sendError(errorReport);
  }

  sendError(errorReport) {
    // Use sendBeacon for reliable delivery
    if (navigator.sendBeacon) {
      navigator.sendBeacon(
        this.apiUrl,
        JSON.stringify(errorReport)
      );
    } else {
      // Fallback to fetch
      fetch(this.apiUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(errorReport),
        keepalive: true
      }).catch(console.error);
    }
  }

  getUserId() {
    // Get from localStorage, cookie, or context
    return localStorage.getItem('userId') || 'anonymous';
  }

  getSessionId() {
    let sessionId = sessionStorage.getItem('sessionId');
    if (!sessionId) {
      sessionId = this.generateId();
      sessionStorage.setItem('sessionId', sessionId);
    }
    return sessionId;
  }

  generateId() {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }
}

2. Enhanced Error Context

class ErrorMonitor {
  // ... previous code ...

  captureError(errorData) {
    const errorReport = {
      ...errorData,
      timestamp: new Date().toISOString(),
      url: window.location.href,
      userAgent: navigator.userAgent,
      environment: this.environment,
      userId: this.getUserId(),
      sessionId: this.getSessionId(),
      // Additional context
      viewport: {
        width: window.innerWidth,
        height: window.innerHeight
      },
      performance: this.getPerformanceMetrics(),
      breadcrumbs: this.getBreadcrumbs(),
      customData: this.customData
    };

    this.sendError(errorReport);
  }

  getPerformanceMetrics() {
    if (!window.performance) return null;

    const timing = window.performance.timing;
    return {
      dns: timing.domainLookupEnd - timing.domainLookupStart,
      tcp: timing.connectEnd - timing.connectStart,
      request: timing.responseStart - timing.requestStart,
      response: timing.responseEnd - timing.responseStart,
      dom: timing.domContentLoadedEventEnd - timing.domLoading,
      load: timing.loadEventEnd - timing.navigationStart
    };
  }

  addBreadcrumb(category, message, data) {
    if (!this.breadcrumbs) {
      this.breadcrumbs = [];
    }
    this.breadcrumbs.push({
      category,
      message,
      data,
      timestamp: Date.now()
    });
    // Keep only last 50 breadcrumbs
    if (this.breadcrumbs.length > 50) {
      this.breadcrumbs.shift();
    }
  }

  getBreadcrumbs() {
    return this.breadcrumbs || [];
  }
}

3. Usage

// Initialize
const monitor = new ErrorMonitor({
  apiUrl: 'https://your-api.com/errors',
  environment: 'production'
});

// Add custom breadcrumbs
monitor.addBreadcrumb('user', 'User clicked button', { buttonId: 'submit' });

// Manually capture errors
try {
  // your code
} catch (error) {
  monitor.captureError({
    message: error.message,
    stack: error.stack,
    type: 'manual'
  });
}

Advanced Features

Source Map Support

To support source maps, you’ll need to:

  1. Upload source maps to your server
  2. Parse stack traces and map them back to original sources
  3. Display original file names and line numbers in your error dashboard

Error Grouping

Implement error grouping to avoid duplicate reports:

generateErrorFingerprint(error) {
  // Create a unique fingerprint based on error characteristics
  const key = `${error.message}-${error.filename}-${error.lineno}`;
  return btoa(key).substring(0, 16);
}

Rate Limiting

Prevent overwhelming your server:

class ErrorMonitor {
  constructor(options = {}) {
    this.maxErrorsPerMinute = options.maxErrorsPerMinute || 10;
    this.errorCount = 0;
    this.errorWindow = Date.now();
    // ...
  }

  shouldSendError() {
    const now = Date.now();
    if (now - this.errorWindow > 60000) {
      this.errorCount = 0;
      this.errorWindow = now;
    }
    return this.errorCount < this.maxErrorsPerMinute;
  }

  captureError(errorData) {
    if (!this.shouldSendError()) {
      console.warn('Error rate limit exceeded');
      return;
    }
    this.errorCount++;
    // ... rest of capture logic
  }
}

Conclusion

Building your own error monitoring SDK is a great learning experience. While production systems may require more features (like error aggregation, alerting, and dashboards), this foundation gives you the core concepts needed to understand how error monitoring works.

Start simple, iterate, and add features as needed!