Как логировать js ошибки с клиента

This article was peer reviewed by Panayiotis «pvgr» Velisarakos, James Wright and Stephan Max. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Detective makes notes while standing over a dead body, surrounded by potential murder weapons

Table of Contents

  • The Console
  • Enhancing the console
  • Logdown
  • console.message
  • Limitations of the console
  • Other Things to Consider
  • Capturing global errors
  • Stack traces
  • TraceKit
  • stacktrace.js
  • Logging Client-Side Errors to the Server
  • Rolling your own server-side logger
  • log4javascript
  • Other libraries
  • Roll Your Own Batch-Compatible Logger
  • Self-Hosted Server-Based Options
  • Errbit
  • SaaS Server-Based Options
  • Loggly
  • {track.js}
  • In Summary

Logging is an important part of any software application, both during active development and when it’s running in production mode.

When you’re working on the server, there are hundreds of libraries available to you regardless of your server-side language of choice, a wide range of storage mechanisms, and all sorts of tools you can use to work with the resulting logs.

However, when it comes to client-side applications, logging is something that often gets overlooked, and the options open to you are rather more limited.

In this article I’ll look at some of the ways in which you can implement logging in a client-side application; particularly in a JavaScript-heavy, single-page application (SPA).

The Console

Perhaps the most common and obvious way to log errors and messages is the console. While it might appear a primitive solution, there’s absolutely no doubt that it’s an invaluable tool for debugging during development, so it’s probably a good place to start.

The implementation of console isn’t always consistent — particularly in IE, perhaps unsurprisingly — but in general there are four key methods available to you:

console.log()
console.info()
console.warn()
console.error()

The output from each of these four methods is subtly different, and most web console implementations (i.e., Dev Tools) allow you to filter messages based on the method used; that is, the logging level.

In order to mitigate the differences between browsers, you can use a wrapper function — such as this one from Paul Irish. The WHATWG is attempting to standardize the console API, but the spec is still at an early stage and unlikely to be implemented for some time.

Tip: If you find that your code is littered with console.log() statements, you might find tools such as grunt-remove-logging or grunt-strip for Grunt, or gulp-strip-debug for Gulp useful for when you move an application into production.

Enhancing the console

There are a couple of libraries you can use to “super-charge” the console.

Logdown

Logdown is a tiny library which provides a few enhancements to the console. You’ll find a demo here.

Logdown allows you to specify prefixes upon instantiation; one possible use for this is to separate out your log messages by module, for example:

var uiLogger = new Logdown({prefix: 'MyApp:UI'});
var networkServiceLogger = new Logdown({prefix: 'MyApp:Network'});

You can then enable or disable the loggers by their prefix, for example:

Logdown.disable('MyApp:UI');
Logdown.enable('MyApp:Network');
Logdown.disable('MyApp:*'); // wildcards are supported, too

Disabling a logger effectively silences it.

Once you’ve instatatied one or more loggers, you can log messages using the log(), warn(), info() and error() methods:

var logger = new Logdown();
logger.log('Page changed');
logger.warn('XYZ has been deprecated in favour of 123');
logger.info('Informational message here');
logger.error('Server API not available!');

Logdown also provides Markdown support:

var logger = new Logdown({markdown: true}); // Technically "markdown: true" isn't required; it's enabled by default
logger.warn('_XYZ_ has been *deprecated* in favour of _123_');

console.message

console.message is another library for beautifying the console’s output.

Here’s a quick animation from the documentation, that shows off some of its features:

console.message in action

Essentially the library provides a chainable interface with methods which allow you to format text, group messages together and make them collapsible, send interactive DOM elements or objects to the log — and even include images.

Limitations of the console

The console is great while you’re building an application and you can have it open in front of you, but unless you happen to be looking over a user’s shoulders, and they happen to have the web console open in their browser, you won’t get to see the result.

What we can do instead is send any errors — or even debug messages during development — to a server somewhere, so that we can access them remotely.

Other Things to Consider

Now that we’ve looked at some of the solutions available to you, let’s look at a few additional considerations.

Capturing global errors

At the very least, it’s worth capturing and logging any unhandled exceptions. You can do this using window.onerror. Here’s a really simple example:

window.onerror = function(message, file, line) {
  console.log('An error occured at line ' + line + ' of ' + file + ': ' + message);
};

Stack traces

Stack traces provide an additional level of detail when an error occurs, which you may wish to make use of in development. There are a couple of libraries that help to build them.

TraceKit

TraceKit allows you to inject stack traces into exceptions, and do something with them (e.g. send them to your server-side logging component) by subscribing to them.

Here’s what the code might look like:

TraceKit.report.subscribe(function yourLogger(errorReport) {
  //send via ajax to server, or use console.error in development
  //to get you started see: https://gist.github.com/4491219
});

Then, in your application:

try {
  /*
   * your application code here
   *
   */
  throw new Error('oops');
} catch (e) {
  TraceKit.report(e); //error with stack trace gets normalized and sent to subscriber
}

stacktrace.js

stacktrace.js is, to quote the documentation , “[a] framework-agnostic, micro-library for getting stack traces in all web browsers”.

It provides a method named printStackTrace() which you can use in an error handler to add a stack trace to your logging function. For example, we could enhance our server-side logger as follows:

function log(data, level) {
  $.post(
    'https://your-app.com/api/logger',
    {
      context     :   navigator.userAgent,
      level       :   level || 'error',
      data         :   data,
      stack_trace :    printStackTrace()
    }
  );
}

Logging Client-Side Errors to the Server

Sending log entries to the server has a number of advantages:

  1. You can capture log entries from your application without being physically at the computer (perfect in production)
  2. You can manage your server-side and client-side logs in the same place, potentially using the same tools
  3. You can set up alerts (e.g. a Slack notification or SMS if a critical error occurs)
  4. Where the console isn’t available or is difficult to view (e.g. when using a mobile’s web view) it’s easier to see what’s going on

Let’s look at a few approaches to this.

Rolling your own server-side logger

In some cases, the simplest solution might be to roll your own server-side logging mechanism.

Here’s an extremely minimal example of the client part using jQuery:

function log(data, level) {
  $.post(
    'https://your-app.com/api/logger',
    {
      context   :   navigator.userAgent,
      level     :   level || 'error',
      data       :   data
    }
  );
}

Some usage examples:

try {
  // some function
} catch (e) {
  log({
    error : e.message
  });
}
log('Informational message here', 'info');

With that in mind, here’s a very basic server-side component to accompany this example, built using Node.js with Express, along with the excellent Winston logging library:

/**
 * Load the dependencies
 */
var express = require( 'express' );
var bodyParser = require('body-parser');
var winston = require( 'winston' );

/**
 * Create the Express app
 */
var app = express();

app.use(bodyParser.urlencoded({ extended: true }));

/**
 * Instantiate the logger
 */
var logger = new ( winston.Logger )({
  transports: [
    new ( winston.transports.Console )(
      { 
        level: 'error'
      }
    ),
    new ( winston.transports.DailyRotateFile )(
      { 
        filename: 'logs/client.log',
        datePattern: '.yyyy-MM-dd'
      }
    )
  ]
});

app.post ('/api/logger', function( req, res, next ) {

  logger.log(
    req.body.level || 'error',
    'Client: ' + req.body.data
  );

  return res.send( 'OK' );

});

var server = app.listen( 8080, function() {
  console.log( 'Listening on port %d', server.address().port );
});

In practice, there are some fundamental limitations to this over-simplified logger:

  1. Most logging mechanisms allow you to configure a minimum logging level so that you can filter out certain entries
  2. It will send log entries immediately, which could lead to your server-side component becoming overloaded

A better way to deal with the second issue is to buffer log entries and send them in batches. A common approach is to use localStorage to store log entries, then send them at particular intervals — be that time-based, when a certain threshold in the number of pending entries is reached, or when the user closes the window or navigates away from your application by utilizing the window.onbeforeunload event.

To get around these issues, let’s look at a ready-made solution for logging from JS apps.

log4javascript

log4javascript is based on the ubiquitous log4j, a Java logging framework which has also been ported to PHP, so if you’re coming from a server-side background you may already have some familiarity with it.

log4javascript uses the concept of appenders, which determine what happens when you call one of its logging methods. The default, PopUpAppender, is arguably rather redundant when you have the dev tools provided by most modern browsers.

What’s probably more useful is the AjaxAppender, which you can use to send log entries back to the server. You can configure the AjaxAppender to send entries in batches at timed intervals using setTimed(), of a certain number using setBatchSize() or when the window is unloaded using setSendAllOnUnload().

log4javascript is available to download from Sourceforge, or the similar Log4js is available on Github. You can refer to the Quickstart to get up-and-running fast.

Here’s an example:

var log = log4javascript.getLogger();
var ajaxAppender = new log4javascript.AjaxAppender('http://example.com/api/logger');
ajaxAppender.setThreshold(log4javascript.Level.ERROR);
ajaxAppender.setBatchSize(10); // send in batches of 10
ajaxAppender.setSendAllOnUnload(); // send all remaining messages on window.beforeunload()
log.addAppender(ajaxAppender);

Alternatively, to send messages at a specific interval:

ajaxAppender.setTimed(true);
ajaxAppender.setTimerInterval(10000); // send every 10 seconds (unit is milliseconds)

Other libraries

If your project uses jQuery, you might want to look into jquery logger which allows you to log via Ajax; however, it doesn’t support batches. It does integrate nicely with Airbrake as a back-end, though.

loglevel is a lightweight and extensible JS-based logging framework, which supports Ajax via the separate serverSend plugin.

Roll Your Own Batch-Compatible Logger

Here’s a simple proof-of-concept of a logger which sends messages in batches. It’s written using vanilla JavaScript with ES6 features.

"use strict";
class Logger {

  // Log levels as per https://tools.ietf.org/html/rfc5424
  static get ERROR()  { return 3; }
  static get WARN()   { return 4; }
  static get INFO()   { return 6; }
  static get DEBUG()  { return 7; }

  constructor(options) {

    if ( !options || typeof options !== 'object' ) {
      throw new Error('options are required, and must be an object');
    }

    if (!options.url) {
      throw new Error('options must include a url property');  
    }

    this.url         =   options.url;
    this.headers     =   options.headers || [ { 'Content-Type' : 'application/json' } ];
    this.level       =   options.level || Logger.ERROR;
    this.batch_size =   options.batch_size || 10;
    this.messages   =   [];

  }

  send(messages) {    
    var xhr = new XMLHttpRequest();
    xhr.open('POST', this.url, true);

    this.headers.forEach(function(header){      
      xhr.setRequestHeader(
        Object.keys(header)[0],
        header[Object.keys(header)[0]]
      );
    });

    var data = JSON.stringify({
      context   :   navigator.userAgent,
      messages  :   messages
    });    
    xhr.send(data);
  }

  log(level, message) {
    if (level <= this.level) {
      this.messages.push({
        level : level,
        message : message
      });      
      if (this.messages.length >= this.batch_size) {
        this.send(this.messages.splice(0, this.batch_size));        
      }
    }
  }

  error(message) {
    this.log(Logger.ERROR, message);
  }

  warn(message) {
    this.log(Logger.WARN, message);
  }

  info(message) {
    this.log(Logger.INFO, message);
  }

  debug(message) {
    this.log(Logger.DEBUG, message);
  }

}

Usage is simple:

var logger = new Logger({
  url : 'http://example.com/api/batch-logger',
  batch_size : 5,
  level : Logger.INFO
});

logger.debug('This is a debug message'); // No effect
logger.info('This is an info message');
logger.warn('This is a warning');
logger.error('This is an error message');
logger.log(Logger.WARN, 'This is a warning');

Self-Hosted Server-Based Options

Errbit

Errbit is an open-source, self-hosted solution for capturing errors. It’s implemented in Ruby and uses MongoDB for storage.

If you want to give Errbit a quick spin, there’s a Chef cookbook or a Dockerfile you can use. There’s also an online demo you can try out.

To sign in to the online demo, use the e-mail demo@errbit-demo.herokuapp.com and the password password.

SaaS Server-Based Options

There are a number of SaaS solutions for logging. These include Loggly, track.js, ErrorCeption, Airbrake and New Relic.

Let’s take a brief look at a few such solutions.

Loggly

Loggly is one of a number of these SaaS solutions. I’m going to use it as an example because it’s easy and free to get started. With the free plan you can log up to 200MB per day, and the data is stored for 7 days.

To use Loggly from a client-side application, you’ll need to include the following snippet:

<script type="text/javascript" src="http://cloudfront.loggly.com/js/loggly.tracker.js" async></script>
<script>
  var _LTracker = _LTracker || [];
  _LTracker.push({'logglyKey': 'YOUR-LOGGING-KEY',
  'sendConsoleErrors' : true });
</script>

Note: You’ll need to replace YOUR-LOGGING-KEY with the value specific to your application, which you’ll get when you’ve signed up and logged in, by going to Source Setup.

If you examine this code, you’ll see that the _LTracker object is initially instantiated as an array. This is a “shim” technique used in many analytics libraries, which means that you can call push() on it before the library has loaded. Any errors or messages you push onto that array will be queued up for when the library becomes available.

Usage is simple:

_LTracker.push(data);

You can use it to send a snippet of text:

_LTracker.push( 'An error occured: ' + e.message );

Or, perhaps more usefully, you can use JSON — for example:

try {
  // some operation
} catch (e) {
  _LTracker.push({
    level   : 'error',
    message : e.message,
    trace   : e.trace,
    context : navigator.userAgent
  });
}

While a fairly basic solution, you could simply use the following code to capture errors:

window.onerror = function(message, file, line) {        
  _LTracker.push({
    context: navigator.userAgent,
    error: message,
    file: file,
    line: line
  });
};

There are some limitations to this approach. Line numbers are virtually useless if you have subtly different builds, or when you’re minifying your JS code.

You’ll also notice in the Loggly snippet above that sendConsoleErrors is set to TRUE, which will automatically log certain errors for you, without having to send them manually. For example, the following will get sent to Loggly if a RequireJS timeout occurs:

{
  "category": "BrowserJsException",
  "exception": {
    "url": "http://example.com/js/require.js",
    "message": "Uncaught Error: Load timeout for modules: mainnhttp://requirejs.org/docs/errors.html#timeout",
    "lineno": 141,
    "colno": 15
  },
  "sessionId": "xyz-123-xyz-123"
}

{track.js}

{track.js} is another SaaS solution for logging.

They offer a free plan; it’s limited to 10 errors per minute, 10,000 hits per month and your data is only stored for 24 hours. The most basic paid plan is $29.99 per month — you’ll find more details on their pricing page.

Note: a “hit” is recorded whenever the library is initialized.

Getting it set up is straightforward:

<!-- BEGIN TRACKJS -->
<script type="text/javascript">window._trackJs = { token: 'YOUR-TOKEN-HERE' };</script>
<script type="text/javascript" src="//d2zah9y47r7bi2.cloudfront.net/releases/current/tracker.js" crossorigin="anonymous"></script>
<!-- END TRACKJS -->

One you’ve pulled in the appropriate file and initialized the library, you can use methods such as track():

/**
  * Directly invokes an error to be sent to TrackJS.
  *
  * @method track
  * @param {Error|String} error The error to be tracked. If error does not have a stacktrace, will attempt to generate one.
  */
trackJs.track("Logical error: state should not be null");

try {
  // do something
} catch (e) {
  trackJs.track(e);
}

Or use the console, which will send the messages to the web service:

trackJs.console.debug("a message"); // debug severity
trackJs.console.log("another message"); // log severity

There’s a lot more you can do with {track.js} — check out the documentation for more information.

In Summary

Client-side logging is often overlooked, but it’s arguably just as important as logging server-side errors. However, there’s no doubt it’s more difficult to setup. There are plenty of options, however, a number of which we’ve looked at during the course of this article.

How do you handle logging in your client-side applications? Have you developed your own approach? Do you use something not covered here? Let me know in the comments.

Серверное логирование клиентских ошибок в JavaScript

  1. Алгоритм
  2. Отлов ошибок

    1. Первый метод. Связка try-catch
    2. Второй метод. Переопределение обработчика события onerror
  3. Передача ошибки на серверную сторону
  4. Логгинг ошибки
  5. Приложение A. SQL Section

— Ты суслика видишь?
— Нет.
— И я нет. А он есть.
(с) ДМБ

Аксиома. В любой программе есть ошибки.
(с) закон Мерфи

Чуть переформулировав: если программа выполняется без ошибок, это еще не означает, что их нет.

К чему я это. Не всегда любое ПО можно оттестировать идеально, что бы в процессе эксплуатации не возникало ошибок. Но если они возникают — их нужно как-то отлавливать. Тем более, если они возникают _уже_ в процессе эксплуатации, а не разработки/отладки.

В PHP скриптах это достигается анализом лог файлов веб сервера. Но как быть с ошибками в JavaScript, который выполняется на стороне пользователя? В данном случае нет никаких лог файлов. Кажется, всё потеряно? Нет. Есть два метода для достижения этих целей.

Сразу сделаю оговорку. Кто-то может не найти в этой статье ничего нового. А для кого-то она может оказаться почти панацеей.

Алгоритм

1. Отловить ошибку браузером клиента и не дать ему возможность ее увидеть. Любым приемлимым способом;
2. Отловленную ошибку необходимо передать на серверную сторону;
3. На стороне сервера с ошибкой можно выполнять любые операции. Нас будет интересовать логгинг в БД и отсылка на почту.

Отлов ошибок

Существует два способа (сделаю поправку: два известных мне способа), как отловить ошибку в JavaScript. Первый — с помощью связки try-catch, второй с помощью переопределения обработчика события onerror. Рассмотрим ниже оба способа чуть подробнее

Первый метод. Связка try-catch

Нужный участок кода оборачивается этой связкой, в нем происходит отлов выбрасываемых ошибок. Можно спокойно применять для каких-нибудь отладочных целей. Но используется, в основном, для отлова ошибок, типа, деление на ноль и прочих, т.к. этой связкой оборачивается конкретный кусок кода. Если кода написано много — это будет долгим и не эффективным занятием.

Плюсы данного метода:
* Не нужно переопределять стандартный обработчик ошибок window.onerror

Минусы данного метода:
* Используется, в основном, для отлова «локальных» ошибок. Необходимо оборачивать этим блоком каждый код, в котором мы хотим отловить ошибку.
* Немного не то, что нужно изначально.

try {
} catch (e) {
 // логгинг
}

Второй метод. Переопределение обработчика события onerror

Позволяет назначить свой обработчик на событие onerror.

Плюсы этого метода:
* Обработчик назначается один раз.
* Если кода написано много, и ошибка может быть где угодно — достаточно одного переопределения в начале скрипта.

Минусов, кажется, нет.

// функция, которая будет выполняться при событии window.onerror. 
// на входе она имеет три параметра:
// - сообщение об ошибке;
// - файл, в котором произошла ошибка;
// - номер строки с ошибкой.
function myErrHandler(message, url, line)
{
   alert ('Error: '+message+' at '+url+' on line '+line);

// что бы не выскочило окошко с сообщение об ошибке - 
// функция должна возвратить true
   return true;
}

// назначаем обработчик для события onerror
window.onerror = myErrHandler;

Итак, ошибка поймана на клиентской стороне. Можно переходить ко второму пункту.

Передача ошибки на серверную сторону

Ошибку нужно передавать на сервер без перезагрузки страницы, без всплывающих и закрывающихся попапов, скрытых фреймов и прочего. Для этого воспользуемся технологией AJAX (Асинхронный JavaScript и XML).

Код ниже желательно вставить первой строчкой в тег body, что бы обработчик был создан до того, как будут выполняться последующие скрипты.

// Обеспечиваем поддержу XMLHttpRequest`а в IE
var xmlVersions = new Array(
    "Msxml2.XMLHTTP.6.0",
    "MSXML2.XMLHTTP.3.0",
    "MSXML2.XMLHTTP",
    "Microsoft.XMLHTTP"
    );
if( typeof XMLHttpRequest == "undefined" ) XMLHttpRequest = function() {
	for(var i in xmlVersions)
	{
		try { return new ActiveXObject(xmlVersions[i]); }
		catch(e) {}
	}
	throw new Error( "This browser does not support XMLHttpRequest." );
};


// Собственно, сам наш обработчик. 
function myErrHandler(message, url, line)
{
	var server_url = window.location.toString().split("/")[2];
	var params = "logJSErr=logJSErr&message="+message+'&url='+url+'&line='+line;
	var req =  new XMLHttpRequest();
	req.open('POST', 'http://'+server_url+'/ajax_chain.php', true)
	req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	req.setRequestHeader("Content-length", params.length);
	req.setRequestHeader("Connection", "close");
	req.send(params); 
	// Чтобы подавить стандартный диалог ошибки JavaScript, 
	// функция должна возвратить true
	return true;
}

//назначаем обработчик для события onerror
window.onerror = myErrHandler;

Ошибку на сервер передали, пользователь ни о чем не догадался. Переходим к плану «b» следующему пункту. (тег STRIKE не работает?)

Логгинг ошибки

Что-то расписывать подробно нет смысла. Ошибка приходит из JS-скрипта в $_POST массиве. Нам необходимо ее забрать, записать в базу и отослать на почту. См реализацию.

p.s. запись в файл была добавлена после того, как в базе стали появляться записи с message==»». немного поразбиравшись нашел в лог файлах апача такую запись: «PHP Notice: iconv(): Detected illegal character in input string in …» и я предположил, что на вход iconv приходила строка в не UTF кодировке. а просматривая содержимое файла можно было бы явно определить, что было не так.

файл ajax_chain.php, находится в корне.

<?php
// Инициируем соединение с базой. Используется библиотека http://dklab.ru/lib/DbSimple/ 
// Можно было бы, конечно, переписать с использованием нативных функций PHP, 
// но уклон, как-бы, идет не на процесс взаимодействия с БД.
require_once($_SERVER['DOCUMENT_ROOT']."/conf.php");
require_once($_SERVER['DOCUMENT_ROOT']."/dbsimple.php");

$db = DbSimple_Generic::connect($cfg['db_dsn']);
if (!empty($cfg['db_prefix'])) {
	define("TABLE_PREFIX", $cfg['db_prefix']);
	$db->setIdentPrefix(TABLE_PREFIX);
}
$db->setErrorHandler('dbErrorHandler');
set_sql_codepage($db);


// собственно, основная часть. проверяем, пришла ли POST-запросом строка logJSErr, 
// если да - то это то, что мы послали из JS скрипта с помощью AJAX`а
if (isset($_POST['logJSErr']))  {
	$notSet = "notSet";
	// Необходимо знать, в каком браузере произошла ошибка и на какой странице
	$httpUserAgent  = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : $notSet;
	$httpReferer    = isset($_SERVER['HTTP_REFERER'])    ? $_SERVER['HTTP_REFERER']    : $notSet;

	// Так же может быть интересно, с какого IP адреса зашел пользователь
	$servRemoteAddr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : $notSet;

	// Прочие переменные, которые могут пригодиться. А могут и нет.
	$httpXForwarded = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $notSet;
	$httpXRealIP    = isset($_SERVER['HTTP_X_REAL_IP'])  ? $_SERVER['HTTP_X_REAL_IP']  : $notSet;
	$servHttpVia    = isset($_SERVER['HTTP_VIA']) ? $_SERVER['HTTP_VIA'] : $notSet;

	// Экранируем пришедший контент - защита "от дурака". 
	// Ибо переменные отсылаются из JS`а в явном виде
	$message = $_POST['message'];
	$url	 = $_POST['url'];
	$line	 = $_POST['line'];

	$message	 = iconv('UTF-8','Windows-1251',$message);
	$message_e   = mysql_escape_string ($message); 
	$url_e  	 = mysql_escape_string ($url);
	$line_e  	 = mysql_escape_string ($line);



// защита от DOS-атак.
// алгоритм работы: 
// имеется массив $limits, определяющий количество разрешенных "сообщений" в единицу 
// времени для конкретного IP адреса. если это корпоративный сайт, и сотрудники выходят
// с NATовского ip-адреса - можно внести его в этот массив, убрав ограничение.
// для всех остальных существует "IP"=='default' - параметры по умолчанию для
// "осталных" пользователей. 
// для каждого IP адреса, с которого приходит "ошибка" - производится запись в сессию
// с временем появления ошибки. при последующем появлении сообщения - сравнивается 
// разница между текущим временем и временем появления прошлой ошибки. если эта разница 
// меньше чем secs/msgs - то не даем "ошибке" пробиться дальше.

	// запускаем сессию
	session_start();
	// при желании можно дописать корректную работу с подсетями. но это не основная задача.
	// и времени на реализацию нет. 
	$limits = array(
				'1.1.1.1' 	 => array('msgs'=>0, 'secs'=>1), // unlimited for 1.1.1.1 ip
				'default'	 => array('msgs'=>2, 'secs'=>1)  // 2 message per second (default)
				);
	// $limits[$ip]['msgs'] - number of messages (0==unlimited)
	// $limits[$ip]['secs'] - per seconds

	// приходит POST. смотрим в сессию. встречался ли данный IP ранее.
	if (isset($_SESSION[$servRemoteAddr]))
	{
		// если он встречался - берем дефолтные или нужные значения msgs & secs
		$msgs = $limits['default']['msgs'];
		$secs = $limits['default']['secs'];
		if (isset($limits[$servRemoteAddr]))
		{
			$msgs = $limits[$servRemoteAddr]['msgs'];
			$secs = $limits[$servRemoteAddr]['secs'];
		}
		// если $msgs == 0 == unlim -> выходим
		if ($msgs == 0)
			break;
		// сравниваем разницу текущего времени и последнего записанного в базу с ($secs/$msgs)
		$delta = microtime(true)-$_SESSION[$servRemoteAddr]['microtime'];
		// если разница меньше чем ($secs/$msgs), то происходит попытка залогировать данные сверх предоставленного лимита
		if ($delta<($secs/$msgs))
			break;
	}
	// если он (IP) не встречался - заносим новое значение. а если встречался - по сути - перезаписываем.
	$_SESSION[$servRemoteAddr]['microtime'] = microtime(true);



	/* temporary section that provides dumping input content to log-file */
	$fname = $_SERVER["DOCUMENT_ROOT"]."logs/js_err.txt";
	$fhandle = fopen($fname, 'ab');
	$content = "";
	$content .= "message: '".$_POST['message'] . "'n url: '".$_POST['url'] . "'n line: '".$_POST['line'] . "'n";
	$content .= "UA: '".$httpUserAgent . "'n Referer: '".$httpReferer . "'n RemoteAddr: '".$servRemoteAddr .
																		 "'n LocalAddr: '".$httpXForwarded . "'n";
	$content .= "date: ".date("Y-m-d H:i:s")."nn";
	fwrite($fhandle, $content);
	fclose($fhandle);
	/* end of temporary section */



	// Пишем ошибку в БД
	$query_dt = date("Y-m-d H:i:s");
	$query = "INSERT INTO `log_javascript` (	`id`, `message`, `url`, `line`, 
				`HTTP_USER_AGENT`, `HTTP_REFERER`, `REMOTE_ADDR`, `HTTP_X_REAL_IP`, `HTTP_X_FORWARDED_FOR`, `HTTP_VIA`, `dt`  )
			VALUES (						NULL , '$message_e', '$url_e', '$line_e', 
		'$httpUserAgent', '$httpReferer', '$servRemoteAddr', '$httpXRealIP', '$httpXForwarded', '$servHttpVia', '$query_dt' 
			); ";
	$logUpdate  = $db->select($query); 


	// И отсылаем ее на email. 
	// Думаю, ничего страшного не будет, если отсылать $message в unescaped-виде.
	ini_set("sendmail_from", "username@ema.il");
	ini_set("SMTP", "smtp.ema.il");
	$subject = 'JavaScript Error';
	$who = "JavaScript master"; 	// по аналогии с Webmaster
	$who_from = "JavaScript Master";

	$rcpt_to = "=?koi8-r?B?" .base64_encode(convert_cyr_string($who, "w","k")). "?= <username@ema.il>";
	$subject = "=?koi8-r?B?" .base64_encode(convert_cyr_string($subject, "w","k"))."?=";

	$message = "
	<html>
	<head>
	  <title>У меня где-то ошибка</title>
	</head>
	<body>
	  <p>Здравствуйте, %username%! <br><br>
	В процессе моего выполнения возникла ошибка: '$message',<br>
	по адресу '$url' на строчке '$line'.<br><br>
	Прочие переменные:<br>
	Referer: $httpReferer;<br>
	User-Agent: $httpUserAgent;<br>
	Remote-Addr: $servRemoteAddr.<br><br>
	Local-Addr: $httpXForwarded.<br><br>
	Время моего запуска: $query_dt<br><br>
	--<br>
	С уважением, <br>выполнявшийся скрипт.<br>
	  </p>
	</body>
	</html>
	";
	$message = convert_cyr_string($message, "w", "k"); 

	$headers  = "Content-Type: text/html; charset=koi8-r" . "n";
	$headers .= "From: =?koi8-r?B?" .base64_encode(convert_cyr_string($who_from, "w","k")). "?= <username@ema.il>" . "n";
	$headers .= "Reply-To: =?koi8-r?B?" .base64_encode(convert_cyr_string($who, "w","k")). "?= <username@ema.il>" . "n";
	$headers .= "X-Mailer: PHP mail tool /" . phpversion();

	mail($rcpt_to, $subject, $message, $headers);

}

?>

Приложение A. SQL Section

CREATE TABLE `log_javascript` (
`id` smallint(5) unsigned NOT NULL auto_increment,
`message` text NOT NULL,
`url` text NOT NULL,
`line` text NOT NULL,
`HTTP_USER_AGENT` text NOT NULL,
`HTTP_REFERER` text NOT NULL,
`REMOTE_ADDR` varchar(255) NOT NULL,
`dt` timestamp NOT NULL default CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

As Eldar points out, you can use e.message to get the message of the exception. However, in Chrome, Firefox and IE10+, you can also get the stack trace using e.stack. The stack trace will include the file and line number of the exception.

So to assemble a string with exception info, you would write something like this:

var exmsg = "";
if (e.message) {
    exmsg += e.message;
}
if (e.stack) {
    exmsg += ' | stack: ' + e.stack;
}

Note that you will only get a stack trace if

  1. the exception was thrown by the browser (such as in response to a
    syntax error);
  2. the exception object is an Error object or has the Error object as its prototype.

So just throwing a string (throw ‘Exception!!’) won’t give you a stack trace.

To take this a bit further, to catch all uncaught exceptions, you would use a window.onerror handler (similar to .Net Application_Error handler in global.asax). The drawback of this used to be (and mostly still is) that this wouldn’t give you access to the actual exception object, so you couldn’t get the stack trace. You’d only get the message, url and line number.

Recently, the standard has been extended to give you the column (great for minified files) and the exception object as well:
http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#errorevent

Right now (April 2014), only Chrome 32 implements all this. IE10+ gives you the column but not the exception object. Firefox 28 still only gives you message, url and line number. Hopefully, this will improve soon. I’ve written about this for the JSNLog project, at:
http://jsnlog.com/Documentation/GetStartedLogging/ExceptionLogging

(disclaimer: I am the author of JSNLog and jsnlog.com)

Secondly, the .Net Exception object supports inner exceptions. It also has a Data property so you can attach key value pairs with for example variable values. I sort of missed that in JavaScript Error object, so I created my own Exception object, also as part of the JSNLog project. It is in the jsnlog.js file in the jsnlog.js Github project (https://github.com/mperdeck/jsnlog.js).

Description is at:
http://jsnlog.com/Documentation/JSNLogJs/Exception

Finally a shameless plug — the JSNLog project I’m working on lets you insert loggers in your JavaScript, and automatically inserts the log messages in your existing server side log. So to log JavaScript exceptions with their stack traces to your server side log, you only need to write:

try {
    ...
} catch (e) {
    JL().fatalException("something went wrong!", e);
}

Логирование — незаменимый инструмент в отладке JS кода. Расставьте логи в критические места и при возникновении ошибки вы сможете посмотреть что произошло в консоли. По логам вы увидите последовательность действий и поймете где произошла ошибка. Но обычно происходит по другому.

Вася следит за логами

Вася следит за логами

Разработчик Вася, использует console.log только когда код не работает. Он думает где это сломалось, и расставляет логи. Потом он перезагружает браузер, смотрит что сломалось не там и идет расставлять еще пачку console.log, а может даже и console.info.

В этом абстрактном Васе я узнаю себя пол года назад. Возможно, вы тоже. Этот подход работает, но можно лучше.

Почему так

Причин для такого поведения много:

  1. Этому не учат в школе/университете/на курсах. Там дают только базовые знания, да и преподаватели не всегда обладают нужной квалификацией.
  2. Если добавлять console.log на каждый чих, то открыв консоль вы ничего не поймете. Получится большая каша.
  3. Если не настроены sourcemap, ставить «точки остановки» долго и не все это умеют
  4. Мы так привыкли. Когда я изучал JavaScript, то в учебных примерах результат выводили через alert()
  5. В JavaScript раньше не хранили много логики. Вычислениями и отрисовкой html занимался сервер.

По данным http archive, средний размер JavaScript файла подключаемого на страницу увеличивается с каждым годом. Приложения разрастаются и становятся сложнее, особенно с приходом Single Page Application.

Как не засрать консоль

Чтобы ориентироваться в логах их важно различать. В консоли хрома в этом нам помогает:

  1. Фильтрация по тексту
  2. Типы логов: .log, .warn, .info, .error
  3. Цвет у сообщения

Для логирования я рекомендую использовать библиотеку debug. Я буду использовать её в примерах далее.

Пример из жизни

Напишем модуль, который будет загружать гугл карты:

{
  'use strict'
  debug = window.debug('service:googlemaps')

  class GoogleMaps {
    constructor() {
      debug('constructor', this)
      this.load()
    }

    load() {
      return new Promise((resolve, reject) => {
        debug('loading:start')
        $.getScript("//maps.googleapis.com/maps/api/js", (data, textStatus) => {
          debug('loading:end', textStatus)

          if (textStatus != 'success') reject(textStatus)
          else resolve()
        })
      })
    }
  }

  GoogleMaps()
}

Чтобы сообщение появились в консоли — укажите какие логи показать. Установив localStorage.debug = '*' мы увидим все сообщения.

Логи модуля GoogleMaps

Логи модуля GoogleMaps

Помимо того, что логи сервиса выделены красным и по ним легко отфильтровать, библиотека так-же показывает разницу во времени между вызовами. Поэтому мы видим что между вызовами конструктора и метода init прошло 2 миллисекунды, а загрузка гуглкарт прошла успешно и заняла 2 секунды.

Как подключить себе

  1. Установите библиотеку через npm, bower или скачайте с github

    $ npm install debug --save
    # или
    $ bower install debug --save
    
  2. Добавьте файл в систему сборки перед другими модулями или вставьте через тег script

    <script src="./path/to/debug.js">
    
  3. Для каждого модуля объявите переменную для дебага. Значение будет добавляться к каждому логу с определенным цветом.

    debug = window.debug('service:googlemaps')
    
  4. Залогируйте что-нибудь

  5. Включите отображение логов в консоли

После выполнения этих действий вы увидите логи в консоли и станете лучше контролировать происходящее в JavaScript. Если у вас раньше не было логирования в проекте, не стоит просить пару дней чтобы его настроить. Подключите библиотеку и когда будете рефакторить старый или писать новый модуль — добавьте логирование туда.

Дополнительный материал

О логировании и console.log рассказал Антон Шувалов выступая на конференции DUMB 2016 в секции FrontTalks.

Антон Шувалов — «Отладка кода в браузере»

Ссылки на записи выступлений с FrontTalks 2016 на хабре, а за прошлый год у меня в статье «Видео и презентации с конференции “FrontTalks” 2015».

Еше можно подписаться на email рассылку новых заметок. Не чаще раза в неделю, без спама.

Время на прочтение
7 мин

Количество просмотров 16K

Специально к старту нового потока курса «Frontend-разработчик» делимся с вами полезным переводом. Автор рассказывает, как использует методы логирования в производственной среде собственного проекта и в чём именно они помогают. Кроме того, нас знакомят с платформой AppSignal, созданной, чтобы напрямую уведомлять разработчика о возникающих у пользователя исключениях в приложении. Подробности под катом.


Я считаю себя инженером внутреннего программного обеспечения — и, как может подтвердить любой внутренний инженер, большая часть нашей жизни уходит на мониторинг, поиск и устранение неисправностей, а также отладку наших приложений. Фундаментальное правило разработки ПО: программное обеспечение будет давать сбои. Новых разработчиков от опытных отличает то, как они планируют эти сбои. Надежное и эффективное логирование — важная часть планирования на случай неудач и, в конечном счёте, смягчения их последствий. Как и в случае разработки бэкенда, логирование может быть полезно в разработке программного обеспечения фронтенда, но оно гораздо больше, чем просто поиск и устранение неисправностей и отладка. Эффективное фронтенд-логирование, кроме того, может сделать разработку продуктивной, быстрой и радостной.

Несмотря на то, что я разделяю и практикую разработку через тестирование, мне нравятся гибкость, богатство информации и надёжность кода, предоставляемые разработчикам фронтенда, которые эффективно используют console.log(). Я решил поделиться некоторыми советами и хитростями, которые изучил и включил в свой рабочий процесс во время работы над Firecode.io в надежде, что некоторые из них помогут сделать ваш рабочий процесс разработки немного продуктивнее и веселее. С удовольствием разделю эти советы на две широкие категории: быстрое и грязное логирование, когда вы активно собираете и отлаживаете приложение, и долговременная запись в лог для понимания, когда ваше приложение работает, как ожидалось, а когда нет.

Советы по быстрому, грязному логированию разработки с console.log()

.

Не используйте console.log()

.
Да, правда. Я не пользуюсь console.log(). Хотя… Ладно. Я пишу обёртки, которые используют console.log. Подробнее об этом — в разделе логирования в производственной среде. Но, если вы хотите логировать что-то в приложении, чтобы увидеть, что происходит, используйте console.trace(). В дополнение ко всему из console.log() этот метод выводит всю трассировку стека, чтобы вы точно знали, откуда идёт сообщение.

Используйте имена вычисляемых свойств ES6 для идентификации объектов и чтобы не путать их с именами переменных.


Это просто — используйте синтаксис вычисляемых свойств ES6: оберните объекты, которые вы хотите логировать, в фигурные скобки вот так: console.log({user}), а не console.log(user). Логирование аккуратное: ключ — имя переменной, а значение — сам объект. Такой подход особенно полезен, когда вы спешите и хотите логировать несколько объектов одной командой console.log().

Отображение уровней логирования: ERROR, WARN, INFO


console.log(param) по умолчанию имеет значение INFO — однако в вашем распоряжении 3 других уровня логирования, которыми вы не должны пренебрегать — console.debug(), console.warning() и console.error(). Помимо различий в форматировании (заметили разные цвета?) консоль разработчика в браузере позволяет легко отфильтровывать логи разных уровней с помощью удобного выпадающего списка, ненужные логи убираются вот так:

При логировании списков элементов пользуйтесь console.table()


Одна из моих любимых функций говорит сама за себя. Если вам когда-нибудь понадобится логировать список объектов, дайте шанс console.table().

Дебажим быстро с помощью debugger


Хотите сэкономить несколько драгоценных секунд? Вместо того чтобы искать файл в консоли разработчика, в который хотите добавить точку останова, впишите в код строку debugger эта строка остановит выполнение кода. Это всё, теперь можно отлаживать и переходить к функциям, как обычно.

Тонкое профилирование с console.profile() и console.time()


Хотите профилировать поток пользователя в вашем приложении, чтобы найти горячие точки? Укажите триггер console.profile(profileName) в начале профилируемого кода и console.profileEnd(profileName) в его конце, чтобы исследовать профиль процессора в потоке.

Кроме того, можно точно измерить, сколько времени занимает выполнение потока, поместив console.time(id) в его начале и console.timeEnd(id) в его конце.

Подсчёт количества выполнений кода через console.count()


Это одна из тех функций, для которых я не нашёл особого применения, тем не менее польза от нее есть: console.count(label) помогает точно узнать, сколько раз выполняется фрагмент кода, это полезно при поиске и устранении состояния гонки и в других ситуациях.

Красивое логирование с помощью CSS


Безусловно, это моя любимая консольная функция, которую я широко использую при логировании в производственной среде (подробнее об этом — в соответствующем разделе). Ближе к сути: мы можем использовать строки форматирования, чтобы форматировать сообщения лога. Здесь %c — модификатор для кода CSS, а всё, что после него, — это сообщение.

Можно стилизовать несколько элементов, расширив строку через %s, вот так:

Я выраженный визуал, мне нравится тратить какое-то время на то, чтобы информационные и отладочные логи выглядели красиво и в то же время были полезны. Я широко использую эту функцию в производственном логировании Firecode.io. И это прекрасная тема для следующего раздела.

Логирование через console.log() в производственной среде

.
Подготовка кода фронтенда включает несколько этапов. Некоторые из них — это уменьшение размера и сжатие вашего кода, генерация дайджестов кэшируемых ассетов и удаление console.log() из кода приложения. Почему удаляется console.log()? Потому что не хочется, чтобы пользователи открывали консоль разработчика и видели логи — дыры в безопасности для пытливых умов.

Но когда приложение используете вы, скорее всего, вы хотите добиться тонкого логирования, чтобы понимать, как ваше приложение работает, а также находить и устранять ошибки. Если ваше приложение используется другими людьми, хочется получать уведомления, когда они сталкиваются с ошибками, чтобы проследить их в коде и исправить их. Вот, что делаю в этом случае я.

Не пользуюсь console.log()


Вместо этого я написал обёртку, которая содержит логику условного логирования, основанную на уровне логирования, который, в свою очередь, основывается на глобальной переменной бэкенда.

Внимание! Впереди фрагменты кода TypeScript. Если вы не знакомы с TypeScript, думайте о нём, как о надмножестве JavaScript с типами, на которых сделан акцент (это грубое упрощение). Иначе говоря, const str = "some string"; превращается в const str: string = "some string" — типы добавляются после имени переменной с двоеточием.

Разрабатывая Firecode.io, я написал собственный фронтенд-фреймворк, который использует RxJS, но содержит знакомые концепции, такие как компоненты, из других популярных фреймворков, к примеру, React и Vue. При этом добавлены и другие концепции: движки для блоков тяжёлого для процессора кода, каналы для сообщений WebSocket и клиенты для HTTP-запросов. Очень важна была визуализация совместной работы всех этих частей, поэтому я реализовал пользовательское форматирование в классе-обёртке Logger, этот класс форматирует и визуально отделяет журналы от каждой части приложения.

Вместо вызова console.log("Cache SET to", {value}) я вызываю Logger.debug("Cache set to", {value}, Framework.Cache). В классе Logger есть перечисление TypeScript, сопоставляющее каждый компонент фреймворка с цветом:

Так я во время разработки визуально концентрируюсь на компонентах приложения. Например, когда я хочу посмотреть, что делает WsRequestCache, я могу «отключиться» от всех логов, кроме бирюзовых.

Защитите логи установкой уровня логирования на бэкенде


Я настроил Firecode.io на включение логирования на уровне отладки по умолчанию для пользователей-администраторов через переменную JavaScript, она устанавливается бэкендом. При этом предприимчивые пользователи все еще могут найти и установить соответствующие флаги в консоли разработчика, чтобы включить точный журнал. Это лучше, чем ситуация, когда каждому пользователю приложения по умолчанию представлены все логи, и лучше, чем постпроцессор, полностью удаляющий логи из приложения в производственной среде. Установка уровня логирования на бэкенде в Ruby on Rails:

const logLevel: number = <%= @app.get_log_level_for_user %>

И в классе Logger:

class Logger {
   ...
   ...
   static info(...) {
     shouldLog(Level.INFO) && console.log(...);
        ...
   }
}

Логируйте возможные ошибки и уведомляйте о них


И последнее, но не менее важное: хочется получать уведомления, когда пользователи сталкиваются с исключениями в приложении, не обязательно при этом с выводом на консоль разработчика. Это возможно. Включите вызов, передающий ваши ошибки в APM-сервис стороннего вендора (например AppSignal) в функцию ошибок вашего логгера. Пример:

class Logger {
   ...
   ...
   static error(e) {
     if (shouldLog(Level.ERROR)) {
       console.error(e);
     }
     appsignal.sendError(e);
   }
}

AppSignal содержит интеграции для передачи ваших ошибок в службы исходящих уведомлений, таких как Slack, PagerDuty и OpsGenie, — вы даже можете подключить инструмент управления проектами, например JIRA или Trello, чтобы автоматически создавать Issues и баги для удобства вашей команды.

Итоги


Я очень надеюсь, что эти советы и истории сделают вашу фронтенд-разработку немного продуктивнее и веселее! Очевидно, что в этом посте я коснулся только поверхности боевого искусства логирования, так что, если у вас есть еще какие-то советы, которыми можно поделиться, я бы с удовольствием прочитал их в своём Twitter.

И два дополнения. Я перестраиваю Firecode.io с нуля, добавляя совершенно новый набор вопросов для собеседований по кодированию JavaScript, Java, Python и Scala.

Если вы заинтересованы в написании кода для подготовки к собеседованию, который адаптируется к вашему стилю обучения и доставляет удовольствие, зарегистрируйтесь, указав свой адрес электронной почты здесь. При запуске нового Firecode.io я предоставлю вам бесплатную трёхмесячную подписку. Я буду размещать больше материалов о создании веб-приложений промышленного масштаба (таких как Firecode.io) с нуля в качестве стороннего проекта. Если вы новичок в JavaScript и хотите понять, как объектноориентированный JavaScript и прототипное наследование работают под капотом, ознакомьтесь с моей любимой книгой по этой теме — The Principles of Object-Oriented JavaScript, и, если вам интересно узнать больше о том, почему вместо JS следует использовать TypeScript, ознакомьтесь с Effective TypeScript.

А помимо специальной литературы вам поможет промокод HABR, добавляющий 10 % к скидке на баннере.

image

  • Обучение профессии Data Science
  • Обучение профессии Data Analyst
  • Онлайн-буткемп по Data Analytics
  • Курс «Python для веб-разработки»

Рекомендуемые статьи

  • Как стать Data Scientist без онлайн-курсов
  • 450 бесплатных курсов от Лиги Плюща
  • Как изучать Machine Learning 5 дней в неделю 9 месяцев подряд
  • Сколько зарабатывает аналитик данных: обзор зарплат и вакансий в России и за рубежом в 2020
  • Machine Learning и Computer Vision в добывающей промышленности

Понравилась статья? Поделить с друзьями:
  • Как ловить ошибки в питоне
  • Как между собой связаны опыт и ошибки
  • Как ловить ошибки в python
  • Как машинка candy выдает ошибку
  • Как лечить ошибку на жестком диске