Skip to main content

Overview

The La Urban logging system provides intelligent, environment-aware console output that automatically adjusts verbosity based on whether the application is running in development or production. This ensures clean production logs while maintaining detailed debugging in development.

Architecture

Logger Class

The logging system is implemented as a singleton class in js/logger.js:
class Logger {
    constructor() {
        this.isDev = this._isDevelopmentEnvironment();
        this._initializeLogger();
    }
    
    _isDevelopmentEnvironment() {
        const hostname = globalThis?.location?.hostname ?? '';
        return hostname === 'localhost' || 
               hostname.startsWith('192.168.') || 
               hostname.includes('.local') ||
               hostname.endsWith('.test');
    }
}

Global Instance

A single logger instance is exported globally for use throughout the application:
// Created at script load
globalThis.logger = new Logger();

// Used anywhere in the application
logger.dev('Debug information');
logger.info('General information');
logger.success('Operation succeeded');
logger.warn('Warning message');
logger.error('Error occurred');
logger.critical('Critical failure');

Environment Detection

Development Detection Logic

The system automatically detects development environments:
hostname === 'localhost'
// Examples:
// - http://localhost:3000
// - http://localhost:8080

Log Levels

Development Logs

These log levels are only visible in development environments:
Purpose: Verbose debugging information
logger.dev('Audio source:', elements.audio.src);
logger.dev('Current state:', state);
logger.dev(`Fade-in: ${startVolume}${targetVolume} in ${duration}ms`);
Output: [DEV] Audio source: https://azura.laurban.cl/listen/laurban/mediaUse case: Technical details, state dumps, flow tracking
Purpose: General information
logger.info('▶️ Play DIRECTO');
logger.info('🎵 Detectado cambio de canción');
logger.info('ℹ️ No hay letras disponibles');
Output: ℹ️ Play DIRECTOUse case: User actions, state changes, informational messages
Purpose: Successful operations
logger.success('✅ Audio source conectado al visualizador');
logger.success('✅ Letras cargadas: 42 líneas');
logger.success('✅ Fade-in completado');
Output: ✅ Audio source conectado al visualizadorUse case: Confirmations, completed operations, positive feedback

Production Logs

These log levels are visible in all environments (development and production):
Purpose: Warning messages that may need attention
logger.warn('⚠️ Stream stalled, attempting to recover...');
logger.warn('⚠️ Using stale cache due to network error');
logger.warn('No se pudo conectar el audio al visualizador (CORS)');
Output: ⚠️ Stream stalled, attempting to recover...Use case: Recoverable errors, degraded functionality, potential issues
Purpose: Error conditions
logger.error('❌ Audio error:', error);
logger.error('❌ Failed to fetch song info:', error);
logger.error('Play failed:', error);
Output: ❌ Audio error: [Error object]Use case: Failed operations, caught exceptions, error states
Purpose: Critical failures requiring immediate attention
logger.critical('⚠️ Max retries reached. Playback failed.');
logger.critical('⚠️ Failed to initialize audio context');
logger.critical('⚠️ No cached data available during outage');
Output: Styled with red background and white text
⚠️ CRÍTICO Max retries reached. Playback failed.
Use case: Unrecoverable errors, system failures, critical bugs

Implementation Details

Console Suppression

In production, the logger suppresses native console methods:
_initializeLogger() {
    // Save original console methods
    this._originalConsole = {
        log: console.log,
        info: console.info,
        warn: console.warn,
        error: console.error,
        debug: console.debug
    };
    
    // In production, suppress verbose logs
    if (!this.isDev) {
        console.log = () => {};
        console.info = () => {};
        console.debug = () => {};
        // Keep warn and error for critical messages
    }
}
Why suppress: Prevents third-party libraries and browser extensions from cluttering production console with unnecessary output.

Log Method Implementation

Each log level uses the appropriate console method:
// Development-only logs
dev(...args) {
    if (this.isDev) {
        this._originalConsole.debug('[DEV]', ...args);
    }
}

info(...args) {
    if (this.isDev) {
        this._originalConsole.info('ℹ️', ...args);
    }
}

success(...args) {
    if (this.isDev) {
        this._originalConsole.log('✅', ...args);
    }
}

// Always-visible logs
warn(...args) {
    this._originalConsole.warn('⚠️', ...args);
}

error(...args) {
    this._originalConsole.error('❌', ...args);
}

critical(...args) {
    const style = 'background: #ff0000; color: white; padding: 2px 5px; border-radius: 3px;';
    this._originalConsole.error('%c⚠️ CRÍTICO', style, ...args);
}

Usage Patterns

Basic Logging

async function playAudio() {
    logger.dev('🎵 playAudio() called');
    logger.dev('Audio source:', elements.audio.src);
    
    try {
        logger.info('▶️ Play DIRECTO');
        await elements.audio.play();
        logger.success('✅ Playback started successfully');
    } catch (error) {
        logger.error('Play failed:', error);
        
        if (retryCount < maxRetries) {
            logger.warn(`Retry attempt ${retryCount}/${maxRetries}`);
        } else {
            logger.critical('Max retries reached. Playback failed.');
        }
    }
}

Conditional Logging

For expensive string operations, guard with environment checks:
// BAD: String concatenation happens even in production
logger.dev('State: ' + JSON.stringify(state));

// GOOD: Skip if not in development (done internally by logger)
logger.dev('State:', state);

// BEST: For very expensive operations
if (logger.isDev) {
    const debugInfo = computeExpensiveDebugInfo();
    logger.dev('Debug info:', debugInfo);
}

Structured Logging

Use multiple arguments for better console formatting:
// Good: Separate arguments
logger.info('Song changed:', {
    artist: song.artist,
    title: song.title,
    duration: song.duration
});

// Better: Named objects expand nicely in console
logger.dev('Playback state', {
    paused: audio.paused,
    currentTime: audio.currentTime,
    volume: audio.volume
});

Browser Extension Error Filtering

The application also filters browser extension errors globally:
// In index.html
const originalError = console.error;
console.error = function(...args) {
    const msg = args.join(' ');
    if (msg.includes('message port closed') || 
        msg.includes('Extension context') || 
        msg.includes('chrome.runtime')) {
        return; // Silenced
    }
    originalError.apply(console, args);
};
This prevents browser extension errors from polluting the application logs.
Global Filters: The extension error filters apply globally, even before the Logger class loads. This ensures a clean console from page load.

Integration with Features

Audio Player Logging

// index.js:1179
async function playAudio() {
    logger.dev(`🎵 playAudio() called ${isMobile ? '(MÓVIL)' : '(ESCRITORIO)'}`);
    logger.info('▶️ Play DIRECTO');
    
    try {
        await elements.audio.play();
        logger.success('✅ Playback started');
    } catch (error) {
        logger.error('Play failed:', error);
    }
}

Lyrics System Logging

// lyrics.js (silent mode)
async function fetchAndLoadLyrics(artist, title, duration, elapsed, silent = false) {
    if (!silent) logger.dev(`🔍 Buscando letras para: ${artist} - ${title}`);
    
    try {
        const data = await fetchLyrics();
        if (data.syncedLyrics) {
            if (!silent) logger.success('✅ Letras cargadas');
        } else {
            if (!silent) logger.info('ℹ️ No hay letras disponibles');
        }
    } catch (error) {
        if (!silent) logger.warn('Error fetching lyrics:', error);
    }
}

Cache Manager Logging

// cache-manager.js
hasChanged(newData) {
    if (this.laurbanData.now_playing?.id !== newData.now_playing?.id) {
        logger.info('🎵 Detectado cambio de canción');
        return true;
    }
    
    if (this.laurbanData.live?.is_live !== newData.live?.is_live) {
        logger.info('🎥 Detectado cambio en estado de live');
        return true;
    }
    
    return false;
}

Console Output Examples

Development Environment

[DEV] 🎵 playAudio() called (ESCRITORIO)
ℹ️ ▶️ Play DIRECTO
✅ Playback started successfully
✅ Audio source conectado al visualizador
[DEV] Fade-in: 0 → 1 in 2500ms
ℹ️ 🎵 Detectado cambio de canción
[DEV] 🔍 Buscando letras para: Bad Bunny - Monaco
⏱️ Tiempo transcurrido: 5.23s de 175s
✅ Letras cargadas: 42 líneas (inicio en 5.23s)
🎤 LETRAS SINCRONIZADAS

Production Environment

(clean - no output unless errors occur)

// Only if error:
❌ Play failed: NotAllowedError: play() can only be initiated by a user gesture.
⚠️ Retry attempt 1/3

// Only if critical:
⚠️ CRÍTICO Max retries reached. Playback failed.

Performance Impact

Development Overhead

  • Negligible: Most logs are simple strings
  • String interpolation: Only evaluated if dev mode
  • Object inspection: Browser handles efficiently

Production Benefits

  • Zero noise: Users see clean console
  • Smaller bundles: Could dead-code eliminate dev logs (future optimization)
  • Faster execution: Suppressed console calls are no-ops

Best Practices

// Use appropriate log levels
logger.dev('Technical details');
logger.info('User-facing events');
logger.success('Positive confirmations');
logger.warn('Recoverable issues');
logger.error('Failed operations');
logger.critical('System failures');

// Include context
logger.error('Fetch failed:', { url, error });

// Use emojis for visual scanning
logger.info('🎵 Song changed');
logger.success('✅ Operation complete');
logger.warn('⚠️ Degraded mode');

Future Enhancements

Potential improvements to the logging system:
  1. Remote Logging: Send critical errors to monitoring service
  2. Log Levels: Configurable verbosity (TRACE, DEBUG, INFO, WARN, ERROR)
  3. Persistence: Save logs to localStorage for debugging
  4. Filtering: Runtime log level adjustment
  5. Timestamps: Include millisecond timestamps
  6. Stack Traces: Capture call stack for errors

Configuration

Currently, the logger has no external configuration. Environment is auto-detected. For custom behavior:
// Force development mode
globalThis.logger.isDev = true;

// Force production mode
globalThis.logger.isDev = false;

// Check current mode
console.log('Development mode:', globalThis.logger.isDev);
Auto-Detection Recommended: Manual mode forcing should only be used for debugging. The automatic environment detection works reliably in all standard scenarios.

Build docs developers (and LLMs) love