Ловим баги в полете: мониторинг и логирование JavaScript приложений

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему ваш JavaScript-код кричит о помощи, а вы не слышите?

Представьте: вы запустили крутое веб-приложение. Пользователи заходят, кликают, всё работает… или нет? На самом деле, где-то в недрах браузера тихо падает ошибка: Uncaught TypeError: Cannot read property 'length' of undefined. Пользователь просто закрывает вкладку и уходит к конкурентам, а вы даже не узнаете, что что-то пошло не так.

Добро пожаловать в реальность frontend-разработки. Без мониторинга и логирования JavaScript приложений вы работаете вслепую. Но не спешите хвататься за голову — сегодня мы разберем, как превратить хаос ошибок в стройную систему оповещений. Вы узнаете, как правильно логировать события, отслеживать производительность и не утонуть в море данных.

Эта статья — ваш шпаргалка по выживанию в production. Поехали!

Console.log — ваш друг или враг?

Начнем с базы. console.log() — это первое, что приходит в голову каждому разработчику. И это нормально. Но если вы используете его в production-коде, остановитесь. Почему? Потому что:

  • Он засоряет консоль пользователя (да, некоторые её открывают).
  • Влияет на производительность при большом количестве вызовов.
  • Не даёт структурированной информации.

Лучше используйте уровни логирования. Вот пример правильного подхода:

// Плохо
console.log('User clicked button');

// Хорошо
console.info('User interaction', { 
    action: 'click', 
    element: '#submit-btn', 
    timestamp: Date.now() 
});

// Для ошибок
console.error('API request failed', { 
    status: 500, 
    endpoint: '/api/users' 
});

// Для предупреждений
console.warn('Deprecated method used', { 
    method: 'fetchData_v1' 
});

Но даже это — лишь вершина айсберга. Настоящий мониторинг JavaScript приложений начинается там, где консоль недоступна.

Структурированное логирование: JSON вам в помощь

Когда ваше приложение разрастается, хаотичные сообщения в консоли превращаются в кашу. Выход — структурированное логирование. Отправляйте данные в формате JSON, чтобы их можно было парсить и анализировать.

Вот как можно сделать простой логгер:

class Logger {
    constructor(context = {}) {
        this.context = context;
    }

    log(level, message, data = {}) {
        const logEntry = {
            timestamp: new Date().toISOString(),
            level,
            message,
            ...this.context,
            ...data
        };

        // В production отправляем на сервер
        if (process.env.NODE_ENV === 'production') {
            this.sendToServer(logEntry);
        } else {
            // В development выводим в консоль
            console[level](`[${level.toUpperCase()}] ${message}`, data);
        }
    }

    sendToServer(entry) {
        // Используем sendBeacon для гарантированной отправки
        navigator.sendBeacon('/api/logs', JSON.stringify(entry));
    }

    info(message, data) { this.log('info', message, data); }
    warn(message, data) { this.log('warn', message, data); }
    error(message, data) { this.log('error', message, data); }
}

// Использование
const logger = new Logger({ appVersion: '1.2.3', userId: getUserId() });
logger.info('Page loaded', { url: window.location.href, loadTime: performance.now() });

Обратите внимание на navigator.sendBeacon() — это лучший способ отправить данные перед уходом пользователя со страницы. Обычные fetch-запросы могут быть отменены.

Отлов ошибок: try/catch — это только начало

Глобальные ошибки — самые коварные. Они могут возникнуть в любом месте, и их легко пропустить. Вот как настроить тотальный мониторинг JavaScript ошибок:

Глобальный обработчик ошибок

// Перехватываем необработанные ошибки
window.addEventListener('error', (event) => {
    logger.error('Global error caught', {
        message: event.message,
        source: event.filename,
        line: event.lineno,
        column: event.colno,
        stack: event.error?.stack
    });
    
    // Не даем ошибке "упасть" в консоль
    event.preventDefault();
});

// Перехватываем rejected promises
window.addEventListener('unhandledrejection', (event) => {
    logger.error('Unhandled promise rejection', {
        reason: event.reason?.message || event.reason,
        stack: event.reason?.stack
    });
    
    event.preventDefault();
});

Monkey patching для fetch и XMLHttpRequest

Хотите логировать все AJAX-запросы? Переопределите методы:

const originalFetch = window.fetch;
window.fetch = async (...args) => {
    const startTime = performance.now();
    
    try {
        const response = await originalFetch(...args);
        const duration = performance.now() - startTime;
        
        logger.info('Fetch request', {
            url: args[0],
            method: args[1]?.method || 'GET',
            status: response.status,
            duration: `${duration.toFixed(2)}ms`
        });
        
        return response;
    } catch (error) {
        logger.error('Fetch failed', {
            url: args[0],
            error: error.message
        });
        throw error;
    }
};

Мониторинг производительности: не дайте пользователю ждать

Медленное приложение = потерянные деньги. Используйте Performance API для отслеживания метрик:

// Отслеживаем Core Web Vitals
function trackWebVitals() {
    // First Contentful Paint
    const paintEntries = performance.getEntriesByType('paint');
    paintEntries.forEach(entry => {
        logger.info(`Paint metric: ${entry.name}`, {
            value: entry.startTime
        });
    });
    
    // Largest Contentful Paint
    if ('largestContentfulPaint' in PerformanceObserver) {
        new PerformanceObserver((list) => {
            const entries = list.getEntries();
            const lastEntry = entries[entries.length - 1];
            logger.info('LCP', { value: lastEntry.startTime });
        }).observe({ type: 'largest-contentful-paint', buffered: true });
    }
    
    // First Input Delay
    if ('firstInput' in PerformanceObserver) {
        new PerformanceObserver((list) => {
            const entry = list.getEntries()[0];
            logger.info('FID', { 
                delay: entry.processingStart - entry.startTime,
                duration: entry.duration 
            });
        }).observe({ type: 'first-input', buffered: true });
    }
}

document.addEventListener('DOMContentLoaded', trackWebVitals);

Инструменты для production-мониторинга

Самописные решения хороши для начала, но в серьёзных проектах нужны специализированные сервисы. Вот топ-3:

  • Sentry — золотой стандарт для отслеживания ошибок. Даёт source maps, стеки вызовов, контекст пользователя.
  • LogRocket — записывает сессии пользователей. Видите не только ошибку, но и что пользователь делал до неё.
  • Datadog RUM — комплексный мониторинг реальных пользователей (Real User Monitoring).

Пример интеграции с Sentry:

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

Sentry.init({
    dsn: 'https://your-dsn@sentry.io/project                

Блоги

Рекомендаций по книгам