Работаю в Почте Mail.ru
Frontend-разработчик
Занимаюсь вебом,
в том числе мониторингом
О чем расскажу
• Счётчик белых экранов
• Счётчик JS-ошибок
6
Ожидание
7
Ожидание
• Проверено разработчиками
8
Ожидание
• Проверено разработчиками
• Проверено тестировщиками
9
Ожидание
• Проверено разработчиками
• Проверено тестировщиками
• Проверено кроссбраузерно
10
Ожидание
• Проверено разработчиками
• Проверено тестировщиками
• Проверено кроссбраузерно
• Проверено на моб. устройствах
11
Ожидание
• Проверено разработчиками
• Проверено тестировщиками
• Проверено кроссбраузерно
• Проверено на моб. устройствах
• Прогнаны регрессы
12
Ожидание
• Проверено разработчиками
• Проверено тестировщиками
• Проверено кроссбраузерно
• Проверено на моб. устройствах
• Прогнаны регрессы
• Прогнаны все автотесты
13
Реальность
14
Тестирования недостаточно
• Есть временные проблемы, локализованные у групп пользователей
• Их вероятность мала, но на большой аудитории они затрагивают сотни
пользователей
16
Счётчик белых экранов
Что за белые экраны?
Белые экраны
19
• Магические
Белые экраны
20
• Магические
• Убивают все живое
Белые экраны
21
• Магические
• Убивают все живое
• Персонажи мифов
Белый экран
22
• В SPA сессия может длиться долго => важен процесс инициализации
приложения
• Белый экран – ситуация, когда инициализация приложения завершилась с
ошибкой
Причины белых экранов
• Браузерное расширение
23
Причины белых экранов
• Браузерное расширение
• Сетевые проблемы
24
Причины белых экранов
• Браузерное расширение
• Сетевые проблемы
• Блокировка
25
Причины белых экранов
• Браузерное расширение
• Сетевые проблемы
• Блокировка
• Ошибка в коде приложения или баг браузера
26
Причины белых экранов
• Браузерное расширение
• Сетевые проблемы
• Блокировка
• Ошибка в коде приложения или баг браузера
• Вы можете столкнуться с чем-то ещё
27
Что дальше?
Нужно определять белые экраны у пользователей!
28
Самый простой случай
<head>
<script>
log('start_init');
</script>
// код инициализации приложения
componentDidMount() {
log('app_init_done');
}
Считаем
разницу
Строим график
30
Почему решение плохое?
• Метрики могут замеряться в разные минуты
31
Почему решение плохое?
• Метрики могут замеряться в разные минуты
• Между ними может быть всё что угодно
32
Почему решение плохое?
• Метрики могут замеряться в разные минуты
• Между ними может быть всё что угодно
if (isMobileUser()) {
window.location.replace(touchUrl);
return;
}
if (isNotSupportedBrowser()) {
window.location.replace(lightUrl);
return;
}
33
Почему решение плохое?
• Метрики могут замеряться в разные минуты
• Между ними может быть всё что угодно
• Нет данных о причине белого экрана
34
Доработка 1 – обработчик onerror
window.onerror = (msg, url, lineNo, columnNo, error) => {
// ... логируем всю полезную информацию ...
log('js_error');
return false;
};
35
Доработка 2 – таймаут инициализации
const TIMEOUT = 25000;
const loadTimeout = setTimeout(() => {
const time = performance.now();
const data = {
// доп. информация
};
log('timeout', time, data);
}, TIMEOUT);
36
Доработка 2 – таймаут инициализации
const TIMEOUT = 25000;
const loadTimeout = setTimeout(() => {
const time = performance.now();
const data = {
// доп. информация
};
log('timeout', time, data);
}, TIMEOUT);
37
КАК ВЫБРАТЬ?
Требуемый уровень качества продукта
• SLA – Service Level Agreement
• Эмпирически на основе текущего состояния проекта
38
Доработка 2 – таймаут инициализации
const TIMEOUT = 25000;
const loadTimeout = setTimeout(() => {
const time = performance.now();
const data = {
// доп. информация
};
log('timeout', time, data);
}, TIMEOUT);
39
ВАЖНО КОРРЕКТИРОВАТЬ
Как узнать скорость соединения пользователя?
40
let speed = 'unknown';
switch (navigator.connection.effectiveType) {
case 'slow-2g':
case '2g':
speed = 'slow';
break;
case '3g':
case '4g':
speed = 'fast';
break;
}
Учитываем загрузку по завершении таймаута
41
const loadTimeout = setTimeout(() => {
const time = performance.now();
const data = {
// доп. информация
};
broken = true;
log('timeout', time, data);
}, TIMEOUT);
Учитываем загрузку по завершении таймаута
42
const loadTimeout = setTimeout(() => {
const time = performance.now();
const data = {
// доп. информация
};
broken = true;
log('timeout', time, data);
}, TIMEOUT);
function appStarted() {
// ...
if (broken) {
log_info['after-broken'] = 1;
}
// ...
}
Доработка 3 – пользователь не дождался загрузки
const closeBeforeInit = () => {
const time = performance.now();
const data = {
// доп. информация
};
log('beforeunload', time, data);
};
window.addEventListener('beforeunload', closeBeforeInit);
43
Доработка 4 – случайно открыли вкладку
const closeBeforeInit = () => {
const time = performance.now();
const data = {
// доп. информация
};
if (time < 1500) {
log('fastclose', time, data);
return;
}
log('beforeunload', time, data);
};
44
Сбрасываем счётчик после инициализации
const resetBrokenCounter = () => {
clearTimeout(loadTimeout);
removeEventListener('beforeunload', closeBeforeInit);
};
45
Доп. информация
const loadTimeout = setTimeout(() => {
const time = performance.now();
const data = {
// доп. информация
};
log('timeout', time, data);
}, TIMEOUT);
ЧТО ЛОГИРУЕМ?
46
Этапы инициализации приложения
// ... code ...
send_init_log('start_init');
// ... code ...
send_init_log('try_load_bundle');
// ... code ...
send_init_log('require_externals');
// ... code ...
47
Причины белых экранов
• Браузерное расширение
• Сетевые проблемы
• Блокировка
• Ошибка в бандле приложения
• Вы можете столкнуться с чем-то ещё
48
Могут быть заблокированы
запросы за файлами
статики приложения!
Решение
49
• Добавить дополнительный location в домене приложения
Клиент Сервер статики
https://cdn.mail.ru/bundle.js CDN
Решение
50
• Добавить дополнительный location в домене приложения
Клиент Сервер статики
https://cdn.mail.ru/bundle.js CDN
Решение
51
• Добавить дополнительный location в домене приложения
Клиент
e.mail.ru
Сервер статики
cdn.mail.ru
Frontend-сервер
https://cdn.mail.ru/bundle.js
https://e.mail.ru/static/bundle.js
CDN
Перезапрос бандла приложения
xhr.request(sourceURL, (error, request) => {
});
52
Перезапрос бандла приложения
xhr.request(sourceURL, (error, request) => {
if (error) {
}
const bundle = request.responseText;
callback(bundle);
});
53
Перезапрос бандла приложения
xhr.request(sourceURL, (error, request) => {
if (error) {
xhr.request(extraURL, (error, request) => {
});
}
const bundle = request.responseText;
callback(bundle);
});
54
Перезапрос бандла приложения
xhr.request(sourceURL, (error, request) => {
if (error) {
xhr.request(extraURL, (error, request) => {
if (error) {
log_fail(error);
}
const bundle = request.responseText;
callback(bundle);
});
}
const bundle = request.responseText;
callback(bundle);
});
55
Нативное решение RequireJS
56
«Errbacks will allow you to detect if a module fails to load, undefine that module, reset
the config to a another location, then try again.
A common use case for this is to use a CDN-hosted version of a library, but if that fails,
switch to loading the file locally.»
Нативное решение RequireJS
57
require.config({
paths: {
jquery: 'https://cdn.mail.ru/libs/jquery.min'
}
});
Нативное решение RequireJS
58
require(['jquery'], ($) => {
// Код
},
Нативное решение RequireJS
59
require(['jquery'], ($) => {
// Код
}, (err) => {
// Получаем список модулей, которые мы не смогли загрузить
const failedId = err.requireModules && err.requireModules[0];
});
Нативное решение RequireJS
60
require(['jquery'], ($) => {
// Код
}, (err) => {
// Получаем список модулей, которые мы не смогли загрузить
const failedId = err.requireModules && err.requireModules[0];
if (failedId === 'jquery') {
require.undef(failedId);
}
});
Нативное решение RequireJS
61
require(['jquery'], ($) => {
// Код
}, (err) => {
// Получаем список модулей, которые мы не смогли загрузить
const failedId = err.requireModules && err.requireModules[0];
if (failedId === 'jquery') {
require.undef(failedId);
require.config({
paths: {
jquery: 'static/jquery'
}
});
require(['jquery'], function () {});
}
});
Нативное решение RequireJS
62
require(['jquery'], ($) => {
// Код
}, (err) => {
// Получаем список модулей, которые мы не смогли загрузить
const failedId = err.requireModules && err.requireModules[0];
if (failedId === 'jquery') {
require.undef(failedId);
require.config({
paths: {
jquery: 'static/jquery'
}
});
require(['jquery'], function () {});
}
});
Нативное решение RequireJS
63
require.config({
paths: {
jquery: [
'https://cdn.mail.ru/libs/jquery.min',
// Если запрос на CDN завершился с ошибкой, загрузить отсюда
'static/jquery'
]
}
});
И это не редкий кейс!
64
Около 20 перезапросов в минуту!
Что ещё логируем?
• Этапы инициализации приложения
• Информацию о загруженных/незагруженных ресурсах
• Информацию о браузере пользователя
• Информацию об устройстве пользователя
• Какие расширения установлены в браузере пользователя
• IP-пользователя, его скорость соединения и т.д.
65
Графики
66
Двигаемся дальше
• Счётчик белых экранов
• Счётчик JS-ошибок
67
Счётчик JS-ошибок
И тут свои особенности
• Тысячи ошибок
69
Категории JS-ошибок. Глобальный обработчик
70
window.onerror = function globalHandler(msg, url, line, column, err) {
};
Категории JS-ошибок. Глобальный обработчик
71
window.onerror = function globalHandler(msg, url, line, column, err) {
const errorObject = {
message: msg,
url: url,
line: line,
column: column,
error: err
};
};
Категории JS-ошибок. Глобальный обработчик
72
window.onerror = function globalHandler(msg, url, line, column, err) {
const errorObject = {
message: msg,
url: url,
line: line,
column: column,
error: err
};
const error = getErrorType(errorObject);
};
Категории JS-ошибок. Глобальный обработчик
73
window.onerror = function globalHandler(msg, url, line, column, err) {
const errorObject = {
message: msg,
url: url,
line: line,
column: column,
error: err
};
const error = getErrorType(errorObject);
log_error(error);
};
Категории JS-ошибок. Глобальный обработчик
74
window.onerror = function globalHandler(msg, url, line, column, err) {
const errorObject = {
message: msg,
url: url,
line: line,
column: column,
error: err
};
const error = getErrorType(errorObject);
log_error(error);
};
Категории JS-ошибок. Определение категории
75
// рекламные баннеры
const ADV_BANNERS = /advertisment.com|adv.com/;
// антивирусы
const KASPERSKY = /[A-Z0-9]{8}(-[A-Z0-9]{4}){3}-[A-Z0-9]{12}/;
// расширения
const EXTENSION = /((^(chrome|file|resource):)|(miscellaneous|extension)_bindings)/;
// скрипты подпроектов
const INTERNAL_1_R = /^/.+.js($|?)/;
const INTERNAL_2_R = /^(https:)?//[a-z0-9.-]+app.ru/.+.js($|?)/;
// ошибки в HTML коде страниц
const INLINE_1_R = /^//;
const INLINE_2_R = /^(https:)?//[a-z0-9.-]+app.ru//;
Категории JS-ошибок. Определение категории
76
function getErrorType(err) {
let type = 'JS';
switch (true) {
// ...
case ADV_BANNERS.test(err.url):
type += '_adv';
break;
// ...
}
return type;
}
Ошибки в рекламных баннерах
77
AdvService.on('render-fail', () => log_error('banners'));
Ошибки в рекламных баннерах
78
AdvService.on('render-fail', () => log_error('banners'));
getBanner(params).then(banner => {
if (banner) {
try {
render(banner);
} catch (error) {
emit('render-fail');
}
}
emit('adv-render-success');
});
Ошибки в пользовательских скриптах
79
• Локализуются у конкретных пользователей
Бандл приложения
• Весь javascript-код приложения
81
<script src="https://cdn.mail.ru/bundle.min.js"></script>
Ошибки в бандле приложения
• Ограничения CORS
82
Сервер статики
Домен: cdn.mail.ru
Фронтенд-сервер
Домен: e.mail.ru
CDN
bundle.js
Ошибки в бандле приложения
• Ограничения CORS
83
Сервер статики
Домен: cdn.mail.ru
Фронтенд-сервер
Домен: e.mail.ru
CDN
bundle.js
При попытке получить
информацию об ошибке
нарушается Same Origin Policy
Ошибки в бандле приложения
• Проблемы с CORS
84
Как решить проблему с CORS
Access-Control-Allow-Origin: https://your.client.com
85
Как решить проблему с CORS
<script src="https://your.cdn.com/script.js" crossorigin="anonymous"></script>
Access-Control-Allow-Origin: https://your.client.com
86
Как решить проблему с CORS
<script src="https://your.cdn.com/script.js" crossorigin="anonymous"></script>
Access-Control-Allow-Origin: https://your.client.com
87
Ну почему…
• Нельзя достучаться до содержимого
88
Ну почему…
• Нельзя достучаться до содержимого
• Нет статуса ответа
89
Ну почему…
• Нельзя достучаться до содержимого
• Нет статуса ответа
• Проблемы с тегом <link>
90
Ну почему…
• Нельзя достучаться до содержимого
• Нет статуса ответа
• Проблемы с тегом <link>
• Нет полного контроля
91
Старый добрый ajax
• Полный контроль над запросом
• Кроссбраузерный
• Предоставляет доступ к содержимому
• Можем динамически определять причину проблемы
• …
• Грузим заранее – исполняем потом
• Грузим параллельно – исполняем в нужном порядке
• Польза ограничивается фантазией
jax
92
Старый добрый ajax
• Полный контроль над запросом
• Кроссбраузерный
• Предоставляет доступ к содержимому
• Можем динамически определять причину проблемы
• …
• Грузим заранее – исполняем потом
• Грузим параллельно – исполняем в нужном порядке
• Польза ограничивается фантазией
jax
93
Получение информации об ошибке с помощью ajax
xhr.request(sourceURL, (request) => {
});
94
Получение информации об ошибке с помощью ajax
xhr.request(sourceURL, (request) => {
const bundle = request.responseText;
});
95
Получение информации об ошибке с помощью ajax
xhr.request(sourceURL, (request) => {
const bundle = request.responseText;
try {
window.eval.call(window, bundle);
callback();
} catch (error) {
}
});
96
Получение информации об ошибке с помощью ajax
xhr.request(sourceURL, (request) => {
const bundle = request.responseText;
try {
window.eval.call(window, bundle);
callback();
} catch (error) {
log_fail(error);
}
});
97
Тут все, что нам нужно? 🤞
Не совсем…
98
Не совсем…
99
• Сообщение об ошибке
• Строчку ошибки
• Позицию ошибки
• Stacktrace
Нельзя кроссбраузерно получить:
Получение информации об ошибке с помощью ajax
xhr.request(sourceURL, (request) => {
const bundle = request.responseText;
try {
window.eval.call(window, bundle);
callback();
} catch (_) {
detectError(bundle);
}
});
100
И как получить информацию?
Получение информации об ошибке с помощью iframe
function detectError(bundle) {
try {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
} catch(_) {}
}
101
Получение информации об ошибке с помощью iframe
function detectError(bundle) {
try {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
const idoc = iframe.contentWindow.document;
idoc.open();
idoc.writeln([
'<script>window.onerror=' + postMessage.toString() + ';</script>',
'<script>' + bundle + '</script>'
].join('n'));
idoc.close();
} catch(_) {}
}
102
Получение информации об ошибке с помощью iframe
function postMessage(msg, url, line, column, err) {
const error = {
url,
msg,
line,
column,
name: err && err.name,
stack: err && err.stack
};
}
103
Получение информации об ошибке с помощью iframe
function postMessage(msg, url, line, column, err) {
const error = {
url,
msg,
line,
column,
name: err && err.name,
stack: err && err.stack
};
const message = JSON.stringify(error);
window.parent.postMessage(message, '*');
}
104
Получение информации об ошибке с помощью iframe
function postMessage(msg, url, line, column, err) {
const error = {
url,
msg,
line,
column,
name: err && err.name,
stack: err && err.stack
};
const message = JSON.stringify(error);
window.parent.postMessage(message, '*');
}
105
Получение информации об ошибке с помощью iframe
window.addEventListener("message", handler);
106
Подводим итоги
• Счётчик белых экранов
• Счётчик JS-ошибок
107
Подводим итоги
• С ростом проекта нужно улучшать и мониторинг
• Больше пользователей – больше причин ошибок
• Чем больше времени вы тратите на детализацию проблем, тем
меньше времени вы тратите на их исправление
108
Попробуйте добавить метрики, о которых я
рассказывал – вы узнаете о своём проекте много нового!
Вопросы?

Артур Хинельцев «Особенности мониторинга высоконагруженных frontend-приложений»

Editor's Notes

  • #2 Всем привет!
  • #3 Всем привет! Как меня уже представили, меня зовут Артур, я работаю в компании Mail.ru Group frontend-разработчиком и сегодня я хочу рассказать вам об особенностях мониторинга высоконагруженных frontend-приложений. Я, как и мой коллега Игорь, работаю над клиентской частью нашей новой почты - Октавиуса.
  • #4 Говоря конкретнее, по мере увеличения аудитории Октавиуса я занимался реализацией подсистемы мониторинга его работы.
  • #5 Поначалу, когда мы только запускали проект, мониторить его было просто. Мало пользователей, маленькая нагрузка, мало метрик. Но постепенно аудитория Октавиуса росла.
  • #6 И чем больше она становилась, тем более разнообразнее становились кейсы, при которых проект у пользователей вел себя не так, как нужно. Я начал сталкиваться с особенностями, которые возникают только у высоконагруженных приложений, и которые мне необходимо было учитывать при решении своих задач. И сегодня я хочу рассказать, что это за особенности, как они отразились на нашей подсистеме мониторинга и главное, почему наш мониторинг стал лучше после того как мы стали эти особенности учитывать.
  • #7 Мой доклад разделен на два блока, в которых я расскажу: о счетчике белых экранов, расскажу что это, зачем он нужен и как мы реализовали его у себя в почте; о том, с какими трудностями можно столкнуться при реализации счетчика js-ошибок, почему стандартных методов их отлова может быть недостаточно и какие неочевидные ошибки можно отловить. Мы выясним, какие метрики можно добавить, как собрать информацию по этим метрикам, и как категоризировать собранную информацию, чтобы после визуализации на графиках мы могли по ним как можно быстрее реагировать на возникающие проблемы с нашим приложением. И все это в контексте именно высоконагруженных приложений.
  • #8 Как мы столкнулись с необходимостью улучшать нашу подсистему мониторинга? Мы в команде, например, хотим, чтобы приложение у пользователей выглядело примерно так.
  • #17 Но дело в том, что тестирования, особенно в крупных проектах, оказывается недостаточно. И тут вина не frontend-разработчиков или тестировщиков. Если у сервиса многомилионная аудитория и большая поддержка старых устройств и старых браузеров, то проблемы, вероятность появления которых исчисляется тысячными долями процента, на такой большой аудитории могут происходить не только после релизов, а вообще когда угодно, и на такой аудитории они будут влиять на сотни пользователей ежедневно.
  • #18 Мы в Октавиусе реализовали такой инструмент и назвали его "Счётчиком белых экранов".
  • #23 Почему они возникают? Почта Mail.ru, как и большинство современных высоконагруженных веб-приложений - это SPA, Single Page Application. Все взаимодействие пользователя в ней и все изменения в интерфейсе приложения происходят внутри одной странички, причем время взаимодействия пользователя с приложением, его сессия работы, длится очень долго. В Почте средняя сессия пользователя длится 2 часа. Именно поэтому процесс инициализации приложения очень важен. И когда по какой-то причине инициализация приложения завершается с ошибкой - это ситуация белого экрана.
  • #24 Например, у пользователя может стоять браузерное расширение или антивирус, которое может повлиять на корректную работу приложения. Чаще всего проблемы бывают с блокировщиками рекламы, которые могут блокировать не запросы за рекламой, а запрос, скажем, за ресурсами приложения. Причем не важно, заблокируется запрос за скриптом, апишный запрос или запрос за CSS-стилями. Если пользователю покажется HTML-страничка без стилей, то она будет также бесполезна, как и белый экран.
  • #25 Также это могут быть проблемы с сетью. Нередко это влияет на целостность подгружаемых скриптов, когда вместо целого бандла приложения грузится только его часть, из-за чего инициализация приложения завершается с ошибкой из-за неожиданного конца файла. Кроме того, никто не застрахован в ошибках настройки сети провайдера, из-за которых запросы за статикой могут попросту не резолвиться.
  • #26 К сожалению, становится не редкостью, и от этого тоже никто не застрахован, когда по ошибке ваш сервис становится недоступным из-за блокировок роскомнадзора.
  • #27 Редко, но случаются ошибки и в самом коде приложения. Тестировщики тоже люди, могут или упустить какой-нибудь кейс, или не написать для него автотест. Бывает, что тестировщик проверяет фичу в одной версии браузера, а в другой версии есть браузерный баг, который не выявится при тестировании, а воспроизведется у пользователя. Ко всему нужно быть готовым, и обязательно такие ошибки тоже нужно отлавливать.
  • #28 Это те причины белых экранов, с которыми мы столкнулись, но они зависят от проекта, в зависимости от функциональности ваших проектов могут появиться и другие проблемы. Итак, мы знаем что у пользователей бывают белые экраны, и мы даже знаем часть причин, почему они возникают. Что дальше?
  • #29 Собственно, наш счетчик белых экранов делает не что иное, как определяет, что у пользователя возникла ситуация белого экрана и отправляет на сервер информацию, по которой можно определить причину проблемы. На основной странице мониторинга проекта у нас даже есть отдельный график, который считает процент пользователей с белым экраном.
  • #30 Перейдем к реализации счетчика белых экранов.
  • #31 Казалось бы, в самом простом случае можно было бы отправлять 2 метрики - 1 метрику где-то в самом верху страницы в секции head, а вторую в конце инициализации нашего приложения, в каждом проекте это место свое. В примере я привел абстрактный componentdidmount компонента приложения. Потом посчитать разницу этих двух метрик и построить по ней график. Чем плох такой подход?
  • #32 Во-первых, эти две метрики могут замеряться в разные минуты и на поминутном графике это будет отображаться постоянной погрешностью. Но это мелочи.
  • #33 Главная проблема в том, что между срабатываниями этих двух метрик могут произойти события, которые не приведут к белому экрану, но которые неправильно будут интерпретироваться как ошибка. Что это за ситуации?
  • #34 Например, в Почте, если мы понимаем, что пользователь зашел с мобильного устройства, то мы сразу редиректим его на мобильную версию приложения. Или если понимаем, что браузер пользователя из числа неподдерживаемых браузеров, то мы редиректим его на так называемую легкую версию Почты. При реализации с двумя метриками мы интерпретируем этот кейс как ситуацию белого экрана, и делаем это ошибочно.
  • #35 И, в-третьих, по таким метрикам мы банально не сможем понять, почему ИМЕННО у пользователя произошел белый экран. Но, как я уже говорил, наша задача не просто отлавливать белые экраны, но и получать информацию, почему они произошли. Как мы можем доработать этот счётчик?
  • #36 Первая ситуация, которую необходимо отлавливать, это js-ошибки в коде инициализации приложения, которые не зависят от разработчиков и тестировщиков. Будь то баг браузера, заблокированный запрос за ресурсом или какая-то ошибка по вине внешнего расширения, это приведет к белому экрану и такие ошибки должны попадать в глобальный обработчик onerror. В этом обработчике доступна информация об ошибке - сообщение об ошибке, урл скрипта или документа, в котором произошла ошибка, номер строки, где она произошла, объект ошибки. Всю эту информацию необходимо логировать, так как она позволяет строить закономерности, которые помогают понимать, где и почему произошла проблема.
  • #37 До этого я говорил, что если пользователю показалась страница без стилей, то это равносильно тому, что ему показался белый экран. В эту же категорию можно добавить ситуацию, когда приложение не загрузилось за какое-то максимально установленное время. Любой пользователь будет не доволен нашим приложением, если он будет очень долго ждать его загрузки. Мы заставляем пользователя страдать, значит с нашей инициализацией приложения что-то не так и нужно считать эту ситуацию за ошибку. Что мы делаем в коде? В начале страницы в секции head мы делаем setTimeout, допустим, на 25 секунд и в момент когда приложение загрузилось, этот таймаут удаляем. В таком случае, если приложение не загрузится, по таймауту сработает callback и мы сможем отправить метрику что приложение не загрузилось по таймауту.
  • #38 Как выбрать таймаут, по которому можно считать, что произошел белый экран?
  • #39 Обычно такие показатели описаны в SLA - Service Level Agreement, это такой договор между заказчиком и поставщиком услуг в котором описан в том числе требуемый уровень качества предоставляемой услуги. В таких проектах как Почта, когда продукт собственный и заказчика как такового нет, требуемый уровень качества определяется самим проектом, но при этом он обязательно должен быть задокументирован и формально принят, чтобы с его помощью можно было анализировать данные мониторинга сервиса, и в нашем случае выбрать таймаут для белого экрана.
  • #40 Но мало этот таймаут выбрать, важно этот таймаут корректировать со временем. Если мы добавили много новой функциональности и осознаем, что время инициализации должно увеличиться, то и мы должны увеличить и наш таймаут. После оптимизаций, если мы видим, что приложение у пользователей стало грузиться быстрее, то нужно понизить этот таймаут с учетом этих оптимизаций. Кроме того, для пользователей с быстрым интернетом и с медленным интернетом данный таймаут также должен отличаться.
  • #41 Определить скорость соединения пользователя можно с помощью объекта Navigator вот таким образом - получить объект connection, в котором есть свойство effectiveType. Оно принимает 4 значения, мы у себя в проекте принимаем, что "slow-2g" и "2g" - это медленное соединение, "3g" и "4g" - быстрое. Такое разделение также поможет в принятии решения по оптимизации приложения, когда приложение у пользователей даже с быстрым интернетом станет грузиться дольше, чем нужно.
  • #42 Кроме того, поскольку в высоконагруженном приложении огромная аудитория, обязательно будут пользователи, у которых сервис грузится дольше установленного таймаута, и их, конечно же, также нужно учитывать. Для этого мы должны завести булеву переменную, в которой будет храниться информация о том, сработал ли счетчик белых экранов.
  • #43 Тогда, в некоторой функции, которая вызывается сразу после инициализации приложения, нужно проверять эту переменную, и если она равна true, то отправлять соответствующую информацию в логи.
  • #44 Если мы считаем белым экраном ситуацию, когда пользователь очень долго ждал, но все таки дождался загрузки приложения, то мы обязаны считать и случаи, когда он не дождался, психанул и закрыл вкладку. В таком случае мы отсылаем метрику в обработчике onbeforeunload страницы, который срабатывает, когда пользователь ее покидает. Отсылаем метрику, естественно, только в том случае, если не сработало событие загрузки приложения.
  • #45 Что можно отловить еще. Представим, пользователь случайно открыл наш сайт. Что он сделает? Он его тут же закрывает. Но наш таймаут то уже запущен, обработчик на закрытие поставлен. Случайность, а на графиках белых экранов погрешность, непорядок. Мы доработали обработчик на закрытие вкладки следующим образом. Предположили, что если пользователь закрывает вкладку быстрее чем за 1,5 секунды, то он открыл вкладку случайно и такую ситуацию мы ошибочно принимаем за белый экран. Выяснилось, что таких пользователей оказалось достаточно много, и этот кейс повысил точность нашего мониторинга.
  • #46 Также важно сбрасывать таймеры и удалять обработчики белых экранов после инициализации приложения или после срабатывания одного из обработчиков. В противном случае мы можем получить двойные срабатывания счётчика и, как следствие, погрешности аналитики.
  • #47 Таким образом, мы понимаем, что проблема есть. Что можно сделать, чтобы лучше понимать, ГДЕ произошла проблема? Везде в примерах кода я приводил некоторую доп. информацию, которую мы логируем при обнаружении белого экрана. Что нужно логировать?
  • #48 В крупных проектах код инициализации приложения обычно достаточно большой и запутанный, поэтому полезно логировать более менее обособленные этапы инициализации. Это может помочь при расследовании возникшего инцидента, так как мы сразу понимаем по логам, какие этапы инициализации прошли, какие не прошли, и из этого уже делаем вывод в какой части инициализации находится искомая проблема.
  • #49 Кроме того, из-за вот этих трех причин белых экранов может случиться так, что запросы за нашим бандлом приложения или за другой статикой заблокируются, поэтому при возникновении белого экрана следует логировать, какие ресурсы были загружены на момент падения приложения, какие - нет. Но увидеть в логах что запросы за статикой были заблокированы это хорошо, но можно и попытаться защититься от этого!
  • #50 Вся наша статика в Почте грузится с серверов CDN, домен которых отличается от домена нашего приложения. Но если запросы к CDN напрямую могут быть заблокированы, то ничто не мешает нам сделать обходной путь: в NGINX реализовать на домен приложения еще один location, который будет проксировать запрос за статикой на наш CDN.
  • #57 Если вы в своем проекте используете RequireJS, существует и нативное решение этой проблемы - так называемые errbacks, или колбеки на случай ошибки загрузки файла. Если открыть документацию, то там как раз указано, что их основное применение - это загрузить файл локально, то есть с домена приложения, в случае падения запроса на CDN.
  • #58 Код из документации очень похож на тот, что я показывал ранее. Где-то наверху в конфиге requirejs мы указываем путь до нужного нам модуля, который грузится с CDN, скажем jquery.
  • #61 Чтобы сделать повторный запрос за ним, нужно вызвать функцию undef. Она сбросит всю информацию о jquery, которая была доступна на момент загрузки. Все модули, которые зависят от jquery и которые не успели загрузиться на момент падения запроса, будут ждать, пока валидный jquery не будет загружен.
  • #62 Потом мы меняем в конфиге путь до модуля на путь в домене приложения и пробуем загрузить его снова.
  • #63 Причем, если перезапрос пройдет успешно, выполнится вот этот колбек, что очень удобно. Но что еще более удобно, так это сокращенный вариант приведенного шаблона для перезапросов!
  • #64 То же самое можно сделать, передав в конфиг не один путь до модуля, а массив путей! Тогда, если загрузка с первого пути не получится, requirejs попытается его загрузить со второго пути!
  • #65 Почему этому кейсу мы уделяем внимание, почему библиотеки наподобие requirejs предлагают нативные решения для него? Потому что этот кейс, к сожалению, не редкий. Конкретно у нас в Почте большое количество пользователей из других стран испытывают проблемы с подключением к нашим серверам статики. И эта доработка помогает всем им без проблем пользоваться нашим сервисом.
  • #66 Вернемся к информации, которую следует логировать при возникновении белого экрана. Все то, что поможет локализовать проблему. С какого браузера заходил пользователь, с какого устройства он заходил, какие браузерные расширения установлены у пользователей, с какой он страны, города, какой его айпишник, какова его скорость соединения. Вся информация в совокупности поможет разобраться в проблеме максимально быстро.
  • #67 Что мы имеем на графиках? На графиках благодаря реализованным несложным доработкам мы уже можем смотреть не только сколько раз у пользователей случается ситуация белого экрана и сопоставить это количество с количеством хитов, но и выделить типы белых экранов. На графике как раз показаны эти типы согласно перечисленным четырем кейсам - ошибки приложения error, ситуации таймаута, закрытие вкладки до загрузки, случайные открытия приложения. Чего мы добились? После внедрения счётчика белых экранов мы узнаем о том, что у пользователей произошла проблема с инициализацией приложения до их обращения в службу поддержки, что как говорил мой коллега, очень важно. С ошибками инициализации понятно. А что насчет ошибок самого приложения?
  • #68 Переходим ко второй секции. Счетчик JS-ошибок.
  • #69 Мы уже касались этого в счетчике белых экранов, когда вешали на страницу обработчик onerror. И для небольшого проекта было бы вполне приемлемо решение, когда все пойманные таким образом js-ошибки выводятся на один общий график, по динамике которого можно судить о возникающих проблемах.
  • #70 Но чем больше становится приложение, тем больше фон ошибок, которые мы не можем исправить, связанные с сетью, блокировками и так далее. В такой ситуации одного общего графика с постоянный огромным фоном ошибок недостаточно, и для локализации проблемы мы в Почте, к примеру, делим все js-ошибки на макрокатегории в зависимости от места их проявления. Какие категории мы выделяем и как мы это делаем?
  • #71 В глобальном обработчике onerror мы отлавливаем все ошибки, формируем объект со всей доступной информацией, получаем категорию ошибки и логируем ее. Как мы получаем категорию ошибки?
  • #75 Как мы получаем категорию ошибки? В этом нам помогает url скрипта, в котором произошла ошибка.
  • #76 У нас есть набор регулярок, с помощью которого мы сопоставляем формат url ошибки с ее категорией. Какие категории мы таким образом определяем? Есть регулярка, проверяющая что ошибка произошла в рекламном баннере. Она содержит домены, чью рекламу мы показываем у себя на странице. Есть регулярка, которая определяет, что ошибка произошла по вине внешнего антивируса. Такое случается, когда антивирусы получают доступ к коду просматриваемой страницы и вставляют туда свои специфичные теги. В таком случае url ошибки будет содержать строку определенного формата, благодаря чему мы можем их определить. Если ошибка произошла из-за какого-нибудь браузерного расширения, то url будет начинаться вместо протокола со слов chrome, file, resource. Также мы определяем ошибки в СВОИХ скриптах, которые не попали в основной бандл приложения, а которые грузятся динамически в зависимости от логики работы и ошибки возникающие в инлайн-скриптах в самом HTML-коде страниц.
  • #77 Ну а в самой функции определения категории ошибки мы последовательно сопоставляем эти регулярки с нашим урлом ошибки и возвращаем найденную категорию или категорию по умолчанию.
  • #78 В случае рекламных баннеров ситуация несколько сложнее и мы отдельно отлавливаем еще и ошибки при попытке отрендерить рекламный блок на страницу. Поскольку код баннера мы пишем не сами, мы не можем быть уверенными, что он не содержит ошибок. Экземпляр класса, который работает с рекламой, мы подписываем на событие render-fail, когда вставить рекламу на страницу не удалось, и в обработчике отправляем ошибку с категорией "баннеры".
  • #79 Откуда это событие появляется? Мы получаем баннер, в try/catch пытается отрендерить его на страницу, если происходит ошибка, то в секции catch эмитим соотвествующее собитие.
  • #80 Нередкая ситуация, когда график ошибок растет по вине самих пользователей. Например, часть наших почтовых пользователей самостоятельно пишут скрипты, которые модифицируют некоторые части нашего приложения. С очередным нашим релизом их скрипты ломаются и на наших графиках отображаются ошибки неизвестного происхождения. В таких случаях мы смотрим по логам и определяем, что все ошибки возникают у конкретных пользователей.
  • #81 С учетом всего этого ваш график js-ошибок может выглядеть примерно таким образом. Мы уже не имеем один общий график, а имеем графики категорий ошибок, по динамике которых мы можем сразу определить, где они произошли.
  • #82 Отдельно я хочу поговорить про категорию ошибок, с которыми возникают пожалуй самое большое проблем в высоконагруженном приложении. Сразу уточню на всякий случай, что я понимаю под бандлом весь javascript код приложения, который грузится одним чаще минифицированным файлом при инициализации приложения.
  • #83 Самое интересное тут в том, что даже если в приложении мы ловим ошибки в бандле приложения, возникает проблема с получением стектрейса этой ошибки и вообще какой-либо расширенной информации о ней. Это происходит из-за того, что чаще всего, и в Почте в том числе, файлы статики находятся на хостах CDN или на различных доменах, которые отличаются от домена вашего сервиса.
  • #84 Поэтому при возникновении ошибок в бандле, когда мы попытаемся получить информацию об ошибке, будет нарушаться политика единого источника и браузер не предоставит доступ к информации об ошибке из-за ограничений безопасности данной политики.
  • #85 Вместо этого мы получим ошибку наподобие "Script error on line 0".
  • #86 Первое, что скорее всего и так у вас уже сделано, это на вашем сервере статики установить заголовки CORS для ваших скриптов, информацию об ошибках в которых вы хотите получать, чтобы они приходили вместе с ответом на клиент.
  • #87 Второе, необходимо модифицировать ваши теги script, добавив атрибут crossorigin, благодаря чему расширенная информация об ошибках будет доступна обработчику window.onerror. Значение anonymous атрибута означает, что вместе с запросом за скриптом не будут отправлены пользовательские куки.
  • #88 Что касается браузерной поддержки, то IE не поддерживает этот атрибут, но он также и не скрывает информацию об ошибках в скриптах полученных с других доменов, поэтому ему поддержка атрибута и не нужна. У остальных популярных браузеров поддержка хорошая с довольно старых версий. И, казалось бы, красивое нативное решение, работает в современных и даже не современных браузерах, добавляется одной строчкой, но оно не подходит именно с точки зрения построения мониторинга.
  • #89 Основная проблема в том, в случае ошибки мы не можем получить содержимое ответа, понять, был ли это битый скрипт, или произошел запрос за несуществующим файлом и в ответе вместо скрипта вернулся html-код страницы 404. Также мы в Почте отлавливаем случай, когда вместо нужного бандла возвращается код формы подключения Wi-Fi, если к нам заходят из метро или кафе. Был период, когда приходилось полностью логировать содержимое ответа для понимания возникшей проблемы. Из обработчиков onerror или onload это содержимое проанализировать не получится.
  • #90 В обработчике onerror тега script нельзя получить статус ответа запроса.
  • #91 Также есть проблемы с загрузкой стилей через тег link. Если загрузятся битые стили или вообще файлы без содержимого, то обработчик onerror тут не сработает, и в таком случае также нужно искать способ получить содержимое этих файлов.
  • #92 Резюмируя - нет полного контроля. В случае ошибки загрузки мы не имеем фактически никакой информации для ее детализации. Что же делать? Придется обратиться к старым друзьям!
  • #93 Старый добрый ajax. С его помощью мы можем сделать запрос за бандлом, из ответа запроса достать его код и заевалить его на странице, что позволит нам в случае ошибки получить о ней подробную информацию! То есть, чем ajax предпочтительнее тега script? Мы получаем полный контроль над запросом, причем кроссбраузерно. Ajax предоставляет нам доступ к содержимому ответа запроса, и мы уже никак не ограничены при его анализе. В совокупности это нам дает возможность динамически определять возникающие ошибки, и даже исправлять их, как было в моем примере с перезапросом бандла приложения.
  • #94 Что еще можно сделать с помощью ajax-a? При желании, с его помощью можно организовать отложенное выполнение бандла, грузить зависимые друг от друга скрипты параллельно, а исполнять их в нужном порядке. В общем его польза ограничивается только вашей фантазией.
  • #95 Как выглядит в коде получение информации об ошибке с помощью ajax?
  • #98 Тут возникает резонный вопрос - в секции catch в объекте error содержится вся необходимая информация, которая нам нужна?
  • #99 Мое исследование показало, что кроссбраузерно получить вот эти данные для мониторинга не получится.
  • #100 В разных браузерах доступна информация разной степени детализации. Самой проблемной точкой является синтаксическая ошибка в бандле, вызванная например сетевой ошибкой, когда в ответ на запрос возвращается только часть файла. В таком случае даже хром не предоставляет никакой информации.
  • #101 И как же нам кроссбраузерно получить информацию об ошибке? Придется исполнять код бандла в iframe-e.
  • #103 Создаем невидимый iframe, добавляем его на страницу, и пишем внутрь него в тег script код нашего бандла. Так как мы точно знаем, что в коде ошибка, то навешиваем еще и обработчик onerror, в который попадет необходимая информация об ошибке.
  • #104 В обработчике onerror формируем объект ошибки и с помощью postMessage отправляем всю информацию родительскому окну.
  • #106 postMessage позволяет общаться друг с другом окнам и ифреймам. Первый параметром мы передаем данные, которые хотим отправить, вторым параметром указываем, кому разрешается получение сообщения. В данном случае мы указываем, что ограничений нет.
  • #107 Кроме того, чтобы приложение могло принимать сообщения от айфрейма с помощью postMessage, мы должны навесить на window родительского окна обработчик на событие message. Таким образом, можно получить подробную информацию о возникшей ошибке. Если же вы считаете, что такой подробной информации об ошибках вам не нужно, то вариант с тегом script и атрибутом crossorigin вам вполне подойдет.
  • #108 Подведем итоги.
  • #109 Есть много хороших практик построения мониторинга, но чем продукт становится старше и чем шире становится ваша поддержка, тем больше становится причин, почему ваш сервис может не работать у пользователя. Поэтому с ростом проекта должен совершенствоваться и ваш мониторинг. Опыт говорит, что чем больше времени мы потратим на детализацию возникающих проблем, тем меньше времени мы потратим на их исправление. А опыт, как известно, самый достоверный источник.
  • #110 Поэтому если у вас нет метрик, о которых я сегодня рассказывал, то попробуйте их добавить, возможно, вы узнаете что-то новое о своем проекте.
  • #111 Всем стабильных проектов! Готов ответить на ваши вопросы.