javascript

exercises

exercises.js⚔
/**
 * 20.4 Debugging Techniques - Exercises
 *
 * Practice debugging and building debugging tools
 */

// ============================================
// EXERCISE 1: Create a Debug Logger
// ============================================

/**
 * Create a comprehensive debug logger with:
 * - Log levels (trace, debug, info, warn, error)
 * - Namespace support
 * - Enable/disable by namespace pattern
 * - Timestamps
 * - Call stack capture
 */

class DebugLogger {
  // Your implementation here
}

/*
// SOLUTION:
class DebugLogger {
    static LEVELS = {
        TRACE: 0,
        DEBUG: 1,
        INFO: 2,
        WARN: 3,
        ERROR: 4,
        SILENT: 5
    };
    
    static currentLevel = DebugLogger.LEVELS.DEBUG;
    static enabledPatterns = new Set(['*']);
    static disabledPatterns = new Set();
    static output = console;
    
    constructor(namespace) {
        this.namespace = namespace;
        this.color = this.hashToColor(namespace);
    }
    
    hashToColor(str) {
        let hash = 0;
        for (let i = 0; i < str.length; i++) {
            hash = str.charCodeAt(i) + ((hash << 5) - hash);
        }
        const hue = Math.abs(hash) % 360;
        return `hsl(${hue}, 70%, 45%)`;
    }
    
    isEnabled() {
        // Check if explicitly disabled
        for (const pattern of DebugLogger.disabledPatterns) {
            if (this.matchPattern(pattern)) return false;
        }
        
        // Check if enabled
        for (const pattern of DebugLogger.enabledPatterns) {
            if (this.matchPattern(pattern)) return true;
        }
        
        return false;
    }
    
    matchPattern(pattern) {
        if (pattern === '*') return true;
        if (pattern === this.namespace) return true;
        
        // Wildcard matching
        if (pattern.endsWith('*')) {
            const prefix = pattern.slice(0, -1);
            return this.namespace.startsWith(prefix);
        }
        
        return false;
    }
    
    formatMessage(level, args) {
        const timestamp = new Date().toISOString();
        const prefix = `[${timestamp}] [${level}] [${this.namespace}]`;
        return { prefix, args };
    }
    
    log(level, levelNum, ...args) {
        if (!this.isEnabled()) return;
        if (levelNum < DebugLogger.currentLevel) return;
        
        const { prefix } = this.formatMessage(level, args);
        
        // Browser styling
        if (typeof window !== 'undefined') {
            const style = `color: ${this.color}; font-weight: bold;`;
            DebugLogger.output.log(`%c${prefix}`, style, ...args);
        } else {
            DebugLogger.output.log(prefix, ...args);
        }
    }
    
    trace(...args) {
        this.log('TRACE', DebugLogger.LEVELS.TRACE, ...args);
        if (this.isEnabled() && DebugLogger.currentLevel <= DebugLogger.LEVELS.TRACE) {
            console.trace();
        }
    }
    
    debug(...args) {
        this.log('DEBUG', DebugLogger.LEVELS.DEBUG, ...args);
    }
    
    info(...args) {
        this.log('INFO', DebugLogger.LEVELS.INFO, ...args);
    }
    
    warn(...args) {
        this.log('WARN', DebugLogger.LEVELS.WARN, ...args);
    }
    
    error(...args) {
        this.log('ERROR', DebugLogger.LEVELS.ERROR, ...args);
    }
    
    // Timing support
    time(label) {
        this._timers = this._timers || {};
        this._timers[label] = performance.now();
    }
    
    timeEnd(label) {
        if (!this._timers?.[label]) return;
        const duration = performance.now() - this._timers[label];
        this.debug(`${label}: ${duration.toFixed(2)}ms`);
        delete this._timers[label];
    }
    
    // Grouping
    group(label) {
        if (!this.isEnabled()) return;
        console.group(`[${this.namespace}] ${label}`);
    }
    
    groupCollapsed(label) {
        if (!this.isEnabled()) return;
        console.groupCollapsed(`[${this.namespace}] ${label}`);
    }
    
    groupEnd() {
        if (!this.isEnabled()) return;
        console.groupEnd();
    }
    
    // Create child logger
    extend(suffix) {
        return new DebugLogger(`${this.namespace}:${suffix}`);
    }
    
    // Static configuration
    static setLevel(level) {
        if (typeof level === 'string') {
            DebugLogger.currentLevel = DebugLogger.LEVELS[level.toUpperCase()] ?? 0;
        } else {
            DebugLogger.currentLevel = level;
        }
    }
    
    static enable(pattern) {
        DebugLogger.enabledPatterns.add(pattern);
    }
    
    static disable(pattern) {
        DebugLogger.disabledPatterns.add(pattern);
    }
    
    static enableAll() {
        DebugLogger.enabledPatterns.add('*');
        DebugLogger.disabledPatterns.clear();
    }
    
    static disableAll() {
        DebugLogger.enabledPatterns.clear();
        DebugLogger.disabledPatterns.add('*');
    }
    
    static reset() {
        DebugLogger.enabledPatterns = new Set(['*']);
        DebugLogger.disabledPatterns = new Set();
        DebugLogger.currentLevel = DebugLogger.LEVELS.DEBUG;
    }
}

// Test the logger
function testDebugLogger() {
    console.log('=== Debug Logger Tests ===\n');
    
    const appLog = new DebugLogger('app');
    const dbLog = new DebugLogger('app:database');
    const authLog = new DebugLogger('app:auth');
    
    // Test basic logging
    appLog.info('Application started');
    appLog.debug('Debug message');
    appLog.warn('Warning message');
    appLog.error('Error message');
    console.log('āœ“ Basic logging works');
    
    // Test levels
    DebugLogger.setLevel('WARN');
    appLog.debug('This should NOT appear');
    appLog.warn('This should appear');
    DebugLogger.setLevel('DEBUG');
    console.log('āœ“ Level filtering works');
    
    // Test namespaces
    DebugLogger.disable('app:database');
    dbLog.info('This should NOT appear');
    authLog.info('This should appear');
    DebugLogger.reset();
    console.log('āœ“ Namespace filtering works');
    
    // Test timing
    appLog.time('operation');
    for (let i = 0; i < 100000; i++) {}
    appLog.timeEnd('operation');
    console.log('āœ“ Timing works');
    
    // Test extend
    const subLog = appLog.extend('submodule');
    subLog.info('From submodule');
    console.log('āœ“ Extend works');
    
    console.log('\n=== Debug Logger Tests Complete ===\n');
}
*/

// ============================================
// EXERCISE 2: Build an Error Tracker
// ============================================

/**
 * Create an error tracking system that:
 * - Captures errors with full context
 * - Parses stack traces
 * - Groups similar errors
 * - Generates reports
 */

class ErrorTracker {
  // Your implementation here
}

/*
// SOLUTION:
class ErrorTracker {
    constructor(options = {}) {
        this.errors = [];
        this.maxErrors = options.maxErrors || 100;
        this.errorGroups = new Map();
        this.handlers = [];
    }
    
    capture(error, context = {}) {
        const errorInfo = {
            id: this.generateId(),
            timestamp: Date.now(),
            isoTime: new Date().toISOString(),
            message: error.message,
            name: error.name,
            stack: error.stack,
            parsedStack: this.parseStack(error.stack),
            context,
            fingerprint: this.getFingerprint(error)
        };
        
        // Add browser/node info
        if (typeof window !== 'undefined') {
            errorInfo.url = window.location.href;
            errorInfo.userAgent = navigator.userAgent;
        }
        
        this.errors.push(errorInfo);
        
        // Group by fingerprint
        if (!this.errorGroups.has(errorInfo.fingerprint)) {
            this.errorGroups.set(errorInfo.fingerprint, []);
        }
        this.errorGroups.get(errorInfo.fingerprint).push(errorInfo);
        
        // Limit stored errors
        if (this.errors.length > this.maxErrors) {
            this.errors.shift();
        }
        
        // Notify handlers
        this.handlers.forEach(h => h(errorInfo));
        
        return errorInfo;
    }
    
    generateId() {
        return 'err_' + Math.random().toString(36).substr(2, 9);
    }
    
    parseStack(stack) {
        if (!stack) return [];
        
        const lines = stack.split('\n').slice(1);
        
        return lines.map(line => {
            // Chrome/Node format: at functionName (file:line:column)
            let match = line.match(/at\s+(.+?)\s+\((.+):(\d+):(\d+)\)/);
            if (match) {
                return {
                    function: match[1],
                    file: match[2],
                    line: parseInt(match[3]),
                    column: parseInt(match[4])
                };
            }
            
            // Anonymous function: at file:line:column
            match = line.match(/at\s+(.+):(\d+):(\d+)/);
            if (match) {
                return {
                    function: '<anonymous>',
                    file: match[1],
                    line: parseInt(match[2]),
                    column: parseInt(match[3])
                };
            }
            
            // Firefox format
            match = line.match(/(.*)@(.+):(\d+):(\d+)/);
            if (match) {
                return {
                    function: match[1] || '<anonymous>',
                    file: match[2],
                    line: parseInt(match[3]),
                    column: parseInt(match[4])
                };
            }
            
            return { raw: line.trim() };
        }).filter(frame => frame.file || frame.raw);
    }
    
    getFingerprint(error) {
        // Create fingerprint from error name and first stack frame
        const stack = this.parseStack(error.stack);
        const firstFrame = stack[0] || {};
        
        return `${error.name}:${error.message.slice(0, 50)}:${firstFrame.file || ''}:${firstFrame.line || ''}`;
    }
    
    onError(handler) {
        this.handlers.push(handler);
        return () => {
            const idx = this.handlers.indexOf(handler);
            if (idx !== -1) this.handlers.splice(idx, 1);
        };
    }
    
    getErrors(options = {}) {
        let errors = [...this.errors];
        
        if (options.name) {
            errors = errors.filter(e => e.name === options.name);
        }
        
        if (options.since) {
            errors = errors.filter(e => e.timestamp >= options.since);
        }
        
        if (options.limit) {
            errors = errors.slice(-options.limit);
        }
        
        return errors;
    }
    
    getGroups() {
        const groups = [];
        
        for (const [fingerprint, errors] of this.errorGroups) {
            groups.push({
                fingerprint,
                count: errors.length,
                sample: errors[0],
                lastSeen: errors[errors.length - 1].timestamp
            });
        }
        
        return groups.sort((a, b) => b.count - a.count);
    }
    
    generateReport() {
        const groups = this.getGroups();
        const total = this.errors.length;
        const unique = groups.length;
        
        const byType = {};
        for (const error of this.errors) {
            byType[error.name] = (byType[error.name] || 0) + 1;
        }
        
        return {
            summary: {
                total,
                unique,
                timeRange: {
                    start: this.errors[0]?.isoTime,
                    end: this.errors[this.errors.length - 1]?.isoTime
                }
            },
            byType,
            topErrors: groups.slice(0, 5).map(g => ({
                message: g.sample.message,
                count: g.count,
                lastSeen: new Date(g.lastSeen).toISOString()
            })),
            generatedAt: new Date().toISOString()
        };
    }
    
    clear() {
        this.errors = [];
        this.errorGroups.clear();
    }
    
    // Global error handler
    install() {
        if (typeof window !== 'undefined') {
            window.addEventListener('error', (event) => {
                this.capture(event.error || new Error(event.message), {
                    type: 'uncaught',
                    filename: event.filename,
                    lineno: event.lineno
                });
            });
            
            window.addEventListener('unhandledrejection', (event) => {
                const error = event.reason instanceof Error ? 
                    event.reason : 
                    new Error(String(event.reason));
                this.capture(error, { type: 'unhandledrejection' });
            });
        }
        
        if (typeof process !== 'undefined') {
            process.on('uncaughtException', (error) => {
                this.capture(error, { type: 'uncaughtException' });
            });
            
            process.on('unhandledRejection', (reason) => {
                const error = reason instanceof Error ? 
                    reason : 
                    new Error(String(reason));
                this.capture(error, { type: 'unhandledRejection' });
            });
        }
    }
}

// Test the error tracker
function testErrorTracker() {
    console.log('=== Error Tracker Tests ===\n');
    
    const tracker = new ErrorTracker();
    
    // Test capture
    try {
        throw new Error('Test error 1');
    } catch (e) {
        tracker.capture(e, { userId: 1 });
    }
    
    try {
        throw new TypeError('Type mismatch');
    } catch (e) {
        tracker.capture(e);
    }
    
    try {
        throw new Error('Test error 1'); // Same as first
    } catch (e) {
        tracker.capture(e, { userId: 2 });
    }
    
    console.log('Captured errors:', tracker.errors.length);
    console.assert(tracker.errors.length === 3, 'Should have 3 errors');
    console.log('āœ“ Error capture works');
    
    // Test grouping
    const groups = tracker.getGroups();
    console.log('Error groups:', groups.length);
    console.assert(groups.length === 2, 'Should have 2 groups');
    console.log('āœ“ Error grouping works');
    
    // Test report
    const report = tracker.generateReport();
    console.log('\nError Report:');
    console.log(JSON.stringify(report, null, 2));
    console.log('āœ“ Report generation works');
    
    console.log('\n=== Error Tracker Tests Complete ===\n');
}
*/

// ============================================
// EXERCISE 3: Create a Performance Profiler
// ============================================

/**
 * Build a performance profiler that:
 * - Measures function execution time
 * - Tracks async operations
 * - Computes statistics
 * - Identifies slow operations
 */

class PerformanceProfiler {
  // Your implementation here
}

/*
// SOLUTION:
class PerformanceProfiler {
    constructor() {
        this.measurements = new Map();
        this.thresholds = {
            slow: 100,
            verySlow: 500
        };
    }
    
    // Wrap function to profile it
    profile(fn, name) {
        const profiler = this;
        const label = name || fn.name || 'anonymous';
        
        return function(...args) {
            const start = performance.now();
            
            try {
                const result = fn.apply(this, args);
                
                if (result instanceof Promise) {
                    return result.finally(() => {
                        profiler.record(label, performance.now() - start);
                    });
                }
                
                profiler.record(label, performance.now() - start);
                return result;
                
            } catch (e) {
                profiler.record(label, performance.now() - start);
                throw e;
            }
        };
    }
    
    // Profile a block of code
    measure(label, fn) {
        const start = performance.now();
        
        try {
            const result = fn();
            
            if (result instanceof Promise) {
                return result.finally(() => {
                    this.record(label, performance.now() - start);
                });
            }
            
            this.record(label, performance.now() - start);
            return result;
            
        } catch (e) {
            this.record(label, performance.now() - start);
            throw e;
        }
    }
    
    // Manual timing
    start(label) {
        this._starts = this._starts || {};
        this._starts[label] = performance.now();
    }
    
    end(label) {
        if (!this._starts?.[label]) return null;
        const duration = performance.now() - this._starts[label];
        delete this._starts[label];
        this.record(label, duration);
        return duration;
    }
    
    record(label, duration) {
        if (!this.measurements.has(label)) {
            this.measurements.set(label, []);
        }
        
        this.measurements.get(label).push({
            duration,
            timestamp: Date.now(),
            slow: duration > this.thresholds.slow,
            verySlow: duration > this.thresholds.verySlow
        });
    }
    
    getStats(label) {
        const data = this.measurements.get(label);
        if (!data || data.length === 0) return null;
        
        const durations = data.map(d => d.duration).sort((a, b) => a - b);
        const sum = durations.reduce((a, b) => a + b, 0);
        const len = durations.length;
        
        return {
            name: label,
            count: len,
            total: sum,
            mean: sum / len,
            min: durations[0],
            max: durations[len - 1],
            median: durations[Math.floor(len / 2)],
            p90: durations[Math.floor(len * 0.9)],
            p95: durations[Math.floor(len * 0.95)],
            p99: durations[Math.floor(len * 0.99)],
            stdDev: Math.sqrt(
                durations.reduce((sq, d) => sq + Math.pow(d - sum/len, 2), 0) / len
            ),
            slowCount: data.filter(d => d.slow).length,
            verySlowCount: data.filter(d => d.verySlow).length
        };
    }
    
    getAllStats() {
        const stats = {};
        for (const label of this.measurements.keys()) {
            stats[label] = this.getStats(label);
        }
        return stats;
    }
    
    getSlowOperations() {
        const slow = [];
        
        for (const [label, data] of this.measurements) {
            const slowOps = data.filter(d => d.slow);
            if (slowOps.length > 0) {
                slow.push({
                    label,
                    count: slowOps.length,
                    maxDuration: Math.max(...slowOps.map(d => d.duration))
                });
            }
        }
        
        return slow.sort((a, b) => b.maxDuration - a.maxDuration);
    }
    
    report() {
        const allStats = this.getAllStats();
        
        console.log('\n╔════════════════════════════════════════════════════════════╗');
        console.log('ā•‘                 Performance Report                          ā•‘');
        console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n');
        
        const rows = Object.values(allStats).map(s => ({
            Name: s.name,
            Count: s.count,
            'Mean (ms)': s.mean.toFixed(2),
            'Min (ms)': s.min.toFixed(2),
            'Max (ms)': s.max.toFixed(2),
            'P95 (ms)': s.p95?.toFixed(2) || '-',
            'Slow': s.slowCount
        }));
        
        console.table(rows);
        
        const slowOps = this.getSlowOperations();
        if (slowOps.length > 0) {
            console.log('\nāš ļø  Slow Operations:');
            console.table(slowOps);
        }
    }
    
    reset() {
        this.measurements.clear();
    }
    
    setThresholds(slow, verySlow) {
        this.thresholds = { slow, verySlow };
    }
}

// Test the profiler
async function testPerformanceProfiler() {
    console.log('=== Performance Profiler Tests ===\n');
    
    const profiler = new PerformanceProfiler();
    profiler.setThresholds(10, 50);
    
    // Test profile wrapper
    function slowFn(n) {
        let result = 0;
        for (let i = 0; i < n; i++) {
            result += Math.sqrt(i);
        }
        return result;
    }
    
    const profiledFn = profiler.profile(slowFn, 'slowFn');
    
    for (let i = 0; i < 5; i++) {
        profiledFn(100000 + i * 50000);
    }
    console.log('āœ“ Profile wrapper works');
    
    // Test measure
    profiler.measure('inline-op', () => {
        let sum = 0;
        for (let i = 0; i < 50000; i++) sum += i;
        return sum;
    });
    console.log('āœ“ Measure works');
    
    // Test async
    const asyncFn = profiler.profile(async () => {
        await new Promise(r => setTimeout(r, 15));
        return 'done';
    }, 'asyncFn');
    
    await asyncFn();
    console.log('āœ“ Async profiling works');
    
    // Test manual timing
    profiler.start('manual');
    for (let i = 0; i < 100000; i++) {}
    profiler.end('manual');
    console.log('āœ“ Manual timing works');
    
    // Show stats
    const stats = profiler.getStats('slowFn');
    console.log('\nslowFn stats:', {
        count: stats.count,
        mean: stats.mean.toFixed(2) + 'ms',
        min: stats.min.toFixed(2) + 'ms',
        max: stats.max.toFixed(2) + 'ms'
    });
    
    // Show report
    profiler.report();
    
    console.log('\n=== Performance Profiler Tests Complete ===\n');
}
*/

// ============================================
// EXERCISE 4: Debug Helper Functions
// ============================================

/**
 * Implement these debugging utilities:
 * 1. deepInspect - Deep object inspection with circular reference handling
 * 2. diff - Compare two objects and show differences
 * 3. traceFunction - Wrap a function to log all calls
 * 4. watchProperty - Monitor property access/changes
 */

function deepInspect(obj, options) {
  // Your implementation
}

function diff(obj1, obj2) {
  // Your implementation
}

function traceFunction(fn, name) {
  // Your implementation
}

function watchProperty(obj, prop) {
  // Your implementation
}

/*
// SOLUTION:

function deepInspect(obj, options = {}) {
    const { depth = 5, showHidden = false, colors = true } = options;
    const seen = new WeakSet();
    
    function format(value, currentDepth, indent = '') {
        if (currentDepth > depth) return '[Max depth]';
        
        // Primitives
        if (value === null) return 'null';
        if (value === undefined) return 'undefined';
        if (typeof value === 'boolean') return String(value);
        if (typeof value === 'number') return String(value);
        if (typeof value === 'string') return `"${value}"`;
        if (typeof value === 'symbol') return value.toString();
        if (typeof value === 'function') {
            return `[Function: ${value.name || 'anonymous'}]`;
        }
        if (typeof value === 'bigint') return value.toString() + 'n';
        
        // Circular reference check
        if (seen.has(value)) return '[Circular]';
        seen.add(value);
        
        // Special objects
        if (value instanceof Date) return value.toISOString();
        if (value instanceof RegExp) return value.toString();
        if (value instanceof Error) {
            return `${value.name}: ${value.message}`;
        }
        if (value instanceof Map) {
            const entries = Array.from(value.entries())
                .map(([k, v]) => `${format(k, currentDepth + 1)} => ${format(v, currentDepth + 1)}`);
            return `Map(${value.size}) { ${entries.join(', ')} }`;
        }
        if (value instanceof Set) {
            const items = Array.from(value)
                .map(v => format(v, currentDepth + 1));
            return `Set(${value.size}) { ${items.join(', ')} }`;
        }
        
        // Arrays
        if (Array.isArray(value)) {
            if (value.length === 0) return '[]';
            
            const items = value.map((v, i) => 
                `${indent}  ${format(v, currentDepth + 1, indent + '  ')}`
            );
            return `[\n${items.join(',\n')}\n${indent}]`;
        }
        
        // Objects
        const keys = showHidden ? 
            Object.getOwnPropertyNames(value) : 
            Object.keys(value);
        
        if (keys.length === 0) return '{}';
        
        const pairs = keys.map(k => {
            const v = value[k];
            return `${indent}  ${k}: ${format(v, currentDepth + 1, indent + '  ')}`;
        });
        
        return `{\n${pairs.join(',\n')}\n${indent}}`;
    }
    
    return format(obj, 0);
}

function diff(obj1, obj2, path = '') {
    const differences = [];
    
    function compare(a, b, currentPath) {
        // Same reference or both primitive and equal
        if (a === b) return;
        
        // Type mismatch
        if (typeof a !== typeof b) {
            differences.push({
                path: currentPath || 'root',
                type: 'type',
                from: typeof a,
                to: typeof b,
                oldValue: a,
                newValue: b
            });
            return;
        }
        
        // Null checks
        if (a === null || b === null) {
            differences.push({
                path: currentPath || 'root',
                type: 'value',
                oldValue: a,
                newValue: b
            });
            return;
        }
        
        // Primitives
        if (typeof a !== 'object') {
            differences.push({
                path: currentPath || 'root',
                type: 'value',
                oldValue: a,
                newValue: b
            });
            return;
        }
        
        // Arrays
        if (Array.isArray(a) && Array.isArray(b)) {
            const maxLen = Math.max(a.length, b.length);
            for (let i = 0; i < maxLen; i++) {
                if (i >= a.length) {
                    differences.push({
                        path: `${currentPath}[${i}]`,
                        type: 'added',
                        newValue: b[i]
                    });
                } else if (i >= b.length) {
                    differences.push({
                        path: `${currentPath}[${i}]`,
                        type: 'removed',
                        oldValue: a[i]
                    });
                } else {
                    compare(a[i], b[i], `${currentPath}[${i}]`);
                }
            }
            return;
        }
        
        // Objects
        const allKeys = new Set([...Object.keys(a), ...Object.keys(b)]);
        
        for (const key of allKeys) {
            const newPath = currentPath ? `${currentPath}.${key}` : key;
            
            if (!(key in a)) {
                differences.push({
                    path: newPath,
                    type: 'added',
                    newValue: b[key]
                });
            } else if (!(key in b)) {
                differences.push({
                    path: newPath,
                    type: 'removed',
                    oldValue: a[key]
                });
            } else {
                compare(a[key], b[key], newPath);
            }
        }
    }
    
    compare(obj1, obj2, path);
    
    return {
        hasDifferences: differences.length > 0,
        count: differences.length,
        differences,
        summary() {
            if (differences.length === 0) return 'Objects are equal';
            return differences.map(d => {
                switch (d.type) {
                    case 'added':
                        return `+ ${d.path}: ${JSON.stringify(d.newValue)}`;
                    case 'removed':
                        return `- ${d.path}: ${JSON.stringify(d.oldValue)}`;
                    case 'value':
                        return `~ ${d.path}: ${JSON.stringify(d.oldValue)} → ${JSON.stringify(d.newValue)}`;
                    case 'type':
                        return `! ${d.path}: type changed ${d.from} → ${d.to}`;
                    default:
                        return `? ${d.path}`;
                }
            }).join('\n');
        }
    };
}

function traceFunction(fn, name) {
    const label = name || fn.name || 'anonymous';
    let callCount = 0;
    
    const traced = function(...args) {
        const callId = ++callCount;
        const indent = '  '.repeat(traced._depth || 0);
        
        console.log(`${indent}→ ${label}(${args.map(a => JSON.stringify(a)).join(', ')}) [call #${callId}]`);
        
        traced._depth = (traced._depth || 0) + 1;
        const start = performance.now();
        
        try {
            const result = fn.apply(this, args);
            
            if (result instanceof Promise) {
                return result.then(r => {
                    const duration = (performance.now() - start).toFixed(2);
                    console.log(`${indent}← ${label} returned: ${JSON.stringify(r)} (${duration}ms)`);
                    traced._depth--;
                    return r;
                }).catch(e => {
                    console.log(`${indent}āœ— ${label} threw: ${e.message}`);
                    traced._depth--;
                    throw e;
                });
            }
            
            const duration = (performance.now() - start).toFixed(2);
            console.log(`${indent}← ${label} returned: ${JSON.stringify(result)} (${duration}ms)`);
            traced._depth--;
            return result;
            
        } catch (e) {
            console.log(`${indent}āœ— ${label} threw: ${e.message}`);
            traced._depth--;
            throw e;
        }
    };
    
    traced.getCallCount = () => callCount;
    traced.reset = () => { callCount = 0; };
    
    return traced;
}

function watchProperty(obj, prop) {
    const watchers = {
        get: [],
        set: []
    };
    
    let value = obj[prop];
    const descriptor = Object.getOwnPropertyDescriptor(obj, prop) || {
        configurable: true,
        enumerable: true
    };
    
    Object.defineProperty(obj, prop, {
        configurable: true,
        enumerable: descriptor.enumerable,
        
        get() {
            const stack = new Error().stack?.split('\n')[2] || '';
            watchers.get.forEach(fn => fn(value, stack));
            console.log(`[GET] ${prop} = ${JSON.stringify(value)}`);
            return value;
        },
        
        set(newValue) {
            const oldValue = value;
            value = newValue;
            const stack = new Error().stack?.split('\n')[2] || '';
            watchers.set.forEach(fn => fn(newValue, oldValue, stack));
            console.log(`[SET] ${prop}: ${JSON.stringify(oldValue)} → ${JSON.stringify(newValue)}`);
        }
    });
    
    return {
        onGet(fn) {
            watchers.get.push(fn);
            return () => {
                const idx = watchers.get.indexOf(fn);
                if (idx !== -1) watchers.get.splice(idx, 1);
            };
        },
        
        onSet(fn) {
            watchers.set.push(fn);
            return () => {
                const idx = watchers.set.indexOf(fn);
                if (idx !== -1) watchers.set.splice(idx, 1);
            };
        },
        
        unwatch() {
            Object.defineProperty(obj, prop, {
                ...descriptor,
                value,
                writable: true
            });
        }
    };
}

// Test the utilities
function testDebugUtilities() {
    console.log('=== Debug Utilities Tests ===\n');
    
    // Test deepInspect
    console.log('--- deepInspect ---');
    const circular = { a: 1 };
    circular.self = circular;
    
    console.log(deepInspect({
        name: 'test',
        nested: { deep: { value: 42 } },
        arr: [1, 2, { x: 3 }],
        fn: function test() {},
        date: new Date(),
        circular
    }));
    console.log('āœ“ deepInspect works');
    
    // Test diff
    console.log('\n--- diff ---');
    const obj1 = { a: 1, b: { c: 2 }, d: [1, 2, 3] };
    const obj2 = { a: 1, b: { c: 3 }, d: [1, 2], e: 'new' };
    
    const result = diff(obj1, obj2);
    console.log(result.summary());
    console.log('āœ“ diff works');
    
    // Test traceFunction
    console.log('\n--- traceFunction ---');
    const add = (a, b) => a + b;
    const tracedAdd = traceFunction(add, 'add');
    tracedAdd(2, 3);
    tracedAdd(5, 7);
    console.log('Call count:', tracedAdd.getCallCount());
    console.log('āœ“ traceFunction works');
    
    // Test watchProperty
    console.log('\n--- watchProperty ---');
    const obj = { count: 0 };
    const watcher = watchProperty(obj, 'count');
    
    obj.count;  // GET
    obj.count = 5;  // SET
    obj.count = 10;  // SET
    
    watcher.unwatch();
    console.log('āœ“ watchProperty works');
    
    console.log('\n=== Debug Utilities Tests Complete ===\n');
}
*/

// ============================================
// RUN EXERCISES
// ============================================

console.log('=== Debugging Techniques Exercises ===');
console.log('');
console.log('Implement the following exercises:');
console.log('1. DebugLogger - Multi-level namespace logger');
console.log('2. ErrorTracker - Error tracking and grouping');
console.log('3. PerformanceProfiler - Function profiling');
console.log('4. Debug utilities - inspect, diff, trace, watch');
console.log('');
console.log('Uncomment solutions to verify your implementation.');

// Export
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    DebugLogger,
    ErrorTracker,
    PerformanceProfiler,
    deepInspect,
    diff,
    traceFunction,
    watchProperty,
  };
}
Exercises - JavaScript Tutorial | DeepML