Chrome Extension Extension Logging Levels — Best Practices

9 min read

Extension Logging Levels

A comprehensive pattern for implementing structured, context-aware logging in Chrome extensions with runtime-controllable log levels.

Log Levels

Define a strict hierarchy of log levels:

enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3,
  FATAL = 4
}

Logger Class

class Logger {
  private context: string;
  private static minLevel: LogLevel = LogLevel.INFO;

  constructor(context: string) {
    this.context = context;
  }

  static async init(): Promise<void> {
    const { logLevel } = await chrome.storage.local.get('logLevel');
    Logger.minLevel = logLevel ?? LogLevel.INFO;
  }

  static setLevel(level: LogLevel): void {
    Logger.minLevel = level;
    chrome.storage.local.set({ logLevel: level });
  }

  private shouldLog(level: LogLevel): boolean {
    return level >= Logger.minLevel;
  }

  private formatMessage(level: string, message: string, data?: unknown): string {
    const timestamp = new Date().toISOString();
    return `[${timestamp}] [${level}] ${this.context} ${message}`;
  }

  private log(level: LogLevel, levelName: string, message: string, data?: unknown, error?: Error): void {
    if (!this.shouldLog(level)) return;

    const colors: Record<string, string> = {
      DEBUG: 'color: #888',
      INFO: 'color: #4CAF50',
      WARN: 'color: #FF9800',
      ERROR: 'color: #F44336',
      FATAL: 'color: #9C27B0; font-weight: bold'
    };

    const prefix = `[${levelName}]`;
    console.log(`%c${prefix}%c ${this.formatMessage(levelName, message)}`, 
      `color: ${colors[levelName]}`, 'color: inherit', data);

    if (error?.stack) {
      console.error(error.stack);
    }
  }

  debug(message: string, data?: unknown): void {
    this.log(LogLevel.DEBUG, 'DEBUG', message, data);
  }

  info(message: string, data?: unknown): void {
    this.log(LogLevel.INFO, 'INFO', message, data);
  }

  warn(message: string, data?: unknown): void {
    this.log(LogLevel.WARN, 'WARN', message, data);
  }

  error(message: string, data?: unknown, error?: Error): void {
    this.log(LogLevel.ERROR, 'ERROR', message, data, error);
  }

  fatal(message: string, data?: unknown, error?: Error): void {
    this.log(LogLevel.FATAL, 'FATAL', message, data, error);
  }
}

Context-Aware Logger

Create loggers for different extension contexts:

const bgLogger = new Logger('[BACKGROUND]');
const popupLogger = new Logger('[POPUP]');

function createContentLogger(tabId: number, url: string): Logger {
  const domain = new URL(url).hostname;
  return new Logger(`[CS:${domain}]`);
}

Conditional Logging

Avoid expensive operations when logging is disabled:

const logger = new Logger('[BACKGROUND]');

// Bad: toString runs even if DEBUG is disabled
logger.debug('Request payload', JSON.stringifyExpensive(payload));

// Good: lazy evaluation
logger.debug('Request payload', () => JSON.stringifyExpensive(payload));

// Or check first
if (logger.shouldLog(LogLevel.DEBUG)) {
  logger.debug('Request payload', expensiveToString(payload));
}

Log Rotation (Circular Buffer)

const MAX_LOG_ENTRIES = 500;

async function addLogEntry(entry: LogEntry): Promise<void> {
  const { logs } = await chrome.storage.session.get('logs');
  const updatedLogs = [...(logs || []), entry].slice(-MAX_LOG_ENTRIES);
  await chrome.storage.session.set({ logs: updatedLogs });
}

interface LogEntry {
  timestamp: string;
  level: string;
  context: string;
  message: string;
  data?: unknown;
  stack?: string;
}

Remote Logging (ERROR+ Only)

async function sendToRemote(entry: LogEntry): Promise<void> {
  if (entry.level === 'DEBUG' || entry.level === 'INFO') return;

  await fetch('https://analytics.example.com/logs', {
    method: 'POST',
    body: JSON.stringify(entry),
    headers: { 'Content-Type': 'application/json' }
  });
}

Production Stripping

Use build-time defines to remove DEBUG logs:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.LOG_LEVEL': JSON.stringify(process.env.LOG_LEVEL || 'INFO')
    })
  ]
};

// logger.ts
const shouldLog = process.env.LOG_LEVEL === 'DEBUG';

Log Viewer Component

In your options page, display recent logs with filtering:

function renderLogViewer(): HTMLElement {
  const container = document.createElement('div');
  container.id = 'log-viewer';

  const filter = document.createElement('select');
  filter.innerHTML = `
    <option value="ALL">All</option>
    <option value="ERROR">Errors Only</option>
    <option value="WARN">Warnings+</option>
  `;

  const logsContainer = document.createElement('div');
  logsContainer.style.cssText = 'max-height: 400px; overflow-y: auto; font-family: monospace;';

  filter.addEventListener('change', () => renderLogs(filter.value));

  async function renderLogs(filterLevel: string): Promise<void> {
    const { logs } = await chrome.storage.session.get('logs');
    logsContainer.innerHTML = (logs || [])
      .filter(l => filterLevel === 'ALL' || levels[l.level] >= levels[filterLevel])
      .map(l => `<div style="color: ${getLevelColor(l.level)}">${l.timestamp} [${l.level}] ${l.context} ${l.message}</div>`)
      .join('');
  }

  container.append(filter, logsContainer);
  renderLogs('ALL');
  return container;
}

function getLevelColor(level: string): string {
  const colors: Record<string, string> = { DEBUG: '#888', INFO: '#4CAF50', WARN: '#FF9800', ERROR: '#F44336', FATAL: '#9C27B0' };
  return colors[level] || '#000';
}

Export Logs

async function exportLogs(): Promise<void> {
  const { logs } = await chrome.storage.session.get('logs');
  const blob = new Blob([JSON.stringify(logs, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  chrome.downloads.download({ url, filename: 'extension-logs.json' });
}

Sentry Integration

import * as Sentry from '@sentry/browser';

function captureError(error: Error, context: string): void {
  Sentry.captureException(error, {
    tags: { context },
    extra: { extension: 'my-extension' }
  });
}

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.