Пошук уроків, статей та іншого контенту
Підключіть сервіс моніторингу помилок, налаштуйте групування, контекст і сповіщення про критичні збої.
Логи показують, що відбулося на сервері. Сервіс моніторингу помилок допомагає швидше зрозуміти:
яка помилка виникла;
у якому середовищі вона сталася;
скільки користувачів і запитів постраждало;
який стек викликів призвів до збою;
чи повторюється проблема;
коли потрібно повідомити команду.
У цьому уроці використаємо Sentry — сервіс, який має SDK для Node.js. Принципи залишаються подібними й для інших сервісів: застосунок надсилає події, сервіс групує їх у проблеми та створює сповіщення.
Створіть проєкт Node.js і встановіть пакет:
npm init -y
npm install @sentry/nodeУ панелі Sentry створіть проєкт для Node.js і скопіюйте його DSN. DSN — це адреса, за якою SDK надсилає події.
Не зберігайте DSN, токени та інші налаштування середовища безпосередньо в коді. Передайте DSN через змінну середовища:
export SENTRY_DSN="https://examplePublicKey@o0.ingest.sentry.io/0"
export NODE_ENV="development"
node app.jsУ production ці змінні зазвичай налаштовують у конфігурації контейнера, CI/CD або хостинг-платформи.
SDK потрібно ініціалізувати на початку запуску застосунку — до створення серверів та імпортування частин застосунку, помилки в яких потрібно відстежувати.
const Sentry = require("@sentry/node");
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV || "development",
sendDefaultPii: false,
});Основні параметри:
dsn — адреса проєкту в Sentry;
environment — середовище, наприклад development, staging або production;
sendDefaultPii — дозвіл на надсилання персональних даних. Без чіткої потреби краще залишати його вимкненим.
Якщо SENTRY_DSN не задано, SDK не зможе надсилати події. Це зручно для локального запуску: застосунок продовжить працювати, але помилки не потраплятимуть у Sentry.
Для надсилання помилки використовуйте captureException:
try {
await performOperation();
} catch (error) {
Sentry.captureException(error);
}Цей виклик записує виняток, стек викликів і доступний контекст. Після цього помилку часто потрібно повторно передати вище або повернути коректну відповідь клієнту:
try {
await performOperation();
} catch (error) {
Sentry.captureException(error);
throw error;
}Не надсилайте одну й ту саму помилку кілька разів без потреби. Наприклад, якщо нижній шар уже виконав captureException, верхній шар не повинен повторно захоплювати той самий виняток.
Повідомлення помилки рідко достатньо для діагностики. Додавайте до події дані, які допомагають відтворити проблему:
ідентифікатор замовлення;
назву операції;
тип платежу;
версію застосунку;
ідентифікатор користувача без зайвих персональних даних;
параметри, що не містять секретів.
Для контексту конкретної події використовуйте withScope:
Sentry.withScope((scope) => {
scope.setTag("operation", "create-order");
scope.setTag("payment_provider", "stripe");
scope.setContext("order", {
id: order.id,
itemCount: order.items.length,
currency: order.currency,
});
Sentry.captureException(error);
});Теги зручно використовувати для пошуку й фільтрації:
scope.setTag("operation", "create-order");
scope.setTag("region", "eu-central");Значення тегів мають бути обмеженими й стабільними. Не використовуйте як тег довільний текст помилки або повний URL з унікальними параметрами. Інакше кількість унікальних значень стане надто великою, а пошук буде менш корисним.
Для пов’язаних об’єктів використовуйте контекст:
scope.setContext("payment", {
provider: "stripe",
currency: "EUR",
amount: 1999,
});Не додавайте до контексту:
паролі;
токени доступу;
номери банківських карток;
cookie;
повні тіла запитів, якщо вони можуть містити персональні дані.
Якщо помилка залежить від конкретного користувача, можна додати його ідентифікатор:
Sentry.withScope((scope) => {
scope.setUser({
id: String(user.id),
});
Sentry.captureException(error);
});Передавайте лише мінімально необхідні дані. Наприклад, для пошуку проблеми часто достатньо внутрішнього id. Email та ім’я не слід надсилати без обґрунтованої потреби.
Сервіс моніторингу групує події в одну проблему. Зазвичай для групування використовуються:
тип винятку;
повідомлення;
стек викликів;
інші характеристики події.
Наприклад, сотні подій із TypeError в одному рядку коду зазвичай утворять одну проблему, а не сотні окремих записів.
Іноді одна логічна помилка має різні повідомлення:
Payment failed for order 1001
Payment failed for order 1002
Payment failed for order 1003Якщо ідентифікатор замовлення входить у повідомлення, події можуть групуватися не так, як потрібно. Для таких випадків можна задати fingerprint:
Sentry.withScope((scope) => {
scope.setFingerprint([
"payment-failed",
error.code || "unknown",
]);
scope.setTag("operation", "payment");
Sentry.captureException(error);
});У цьому прикладі помилки з однаковим error.code та однаковим fingerprint потраплятимуть до однієї групи.
Власне групування потрібно використовувати обережно:
не додавайте до fingerprint випадкові ідентифікатори;
не використовуйте повний текст динамічного повідомлення;
не об’єднуйте в одну групу помилки, які мають різні причини;
не змінюйте fingerprint без потреби, інакше історія проблеми розділиться на кілька груп.
Нижче наведено мінімальний HTTP-сервер. Він:
ініціалізує Sentry;
додає контекст запиту;
генерує помилку для маршруту /error;
використовує fingerprint для помилок платежів;
коректно завершує надсилання події перед завершенням процесу.
const http = require("node:http");
const Sentry = require("@sentry/node");
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV || "development",
sendDefaultPii: false,
});
function sendJson(response, statusCode, body) {
response.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8",
});
response.end(JSON.stringify(body));
}
async function handleRequest(request, response) {
if (request.url === "/health") {
sendJson(response, 200, { status: "ok" });
return;
}
if (request.url === "/error") {
const error = new Error("Не вдалося отримати профіль користувача");
error.code = "PROFILE_SERVICE_UNAVAILABLE";
throw error;
}
if (request.url === "/payment") {
const error = new Error("Помилка під час оплати");
error.code = "PAYMENT_PROVIDER_UNAVAILABLE";
Sentry.withScope((scope) => {
scope.setTag("operation", "payment");
scope.setTag("provider", "example-provider");
scope.setFingerprint(["payment-failed", error.code]);
scope.setContext("payment", {
currency: "EUR",
amount: 1999,
});
Sentry.captureException(error);
});
sendJson(response, 502, {
error: "Платіж тимчасово недоступний",
});
return;
}
sendJson(response, 404, { error: "Маршрут не знайдено" });
}
const server = http.createServer(async (request, response) => {
try {
await handleRequest(request, response);
} catch (error) {
Sentry.withScope((scope) => {
scope.setTag("route", request.url || "unknown");
scope.setTag("method", request.method || "unknown");
scope.setContext("request_info", {
url: request.url,
method: request.method,
});
Sentry.captureException(error);
});
sendJson(response, 500, {
error: "Внутрішня помилка сервера",
});
}
});
server.listen(3000, () => {
console.log("Сервер запущено на http://localhost:3000");
});
process.on("SIGTERM", async () => {
server.close(async () => {
// Даємо SDK час надіслати події, які залишилися в черзі.
await Sentry.close(2000);
process.exit(0);
});
});Запустіть сервер:
SENTRY_DSN="ваш_DSN" NODE_ENV="development" node app.jsПеревірте маршрути:
curl http://localhost:3000/health
curl http://localhost:3000/error
curl http://localhost:3000/paymentПісля звернення до /error або /payment подія має з’явитися у відповідному проєкті Sentry.
Не кожне повідомлення є критичною помилкою. Для подій можна використовувати рівні:
debug — діагностична інформація;
info — звичайна інформація;
warning — потенційна проблема;
error — помилка операції;
fatal — критичний збій застосунку.
Наприклад:
Sentry.withScope((scope) => {
scope.setLevel("fatal");
scope.setTag("component", "database");
Sentry.captureException(error);
});Рівень повинен відповідати наслідкам події. Не позначайте кожну помилку як fatal, інакше критичні сповіщення втратять сенс.
Сповіщення налаштовуються в панелі сервісу моніторингу. Практичне правило — повідомляти команду лише про події, які потребують реакції.
Для критичних збоїв корисними умовами є:
подія має рівень fatal;
подія виникла в середовищі production;
проблема виникла вперше;
кількість подій перевищила поріг за певний проміжок часу;
кількість постраждалих користувачів перевищила допустиме значення;
помилка належить до конкретного компонента або операції.
Канал сповіщення залежить від процесів команди:
email — для невеликої кількості важливих повідомлень;
Slack або інший командний месенджер — для оперативної роботи;
система чергувань — для проблем, які потребують реакції поза робочим часом.
Окремо налаштуйте різні правила для staging і production. Помилки тестового середовища не повинні створювати такі самі термінові сповіщення, як production-збої.
Хороше сповіщення має містити:
назву проблеми;
середовище;
кількість подій;
кількість користувачів;
останній час виникнення;
посилання на стек викликів;
відповідальну команду або компонент.
Після підключення перевірте:
Подія з’являється у правильному проєкті.
Подія має правильне середовище.
Стек викликів читається та містить корисні кадри.
Теги й контекст доступні у деталях події.
Схожі помилки потрапляють до однієї групи.
Різні причини не об’єднуються помилково.
Критичне сповіщення надходить потрібній команді.
У події немає паролів, токенів та іншої чутливої інформації.
Після завершення процесу події, що залишилися в черзі, не втрачаються.
Тестувати потрібно щонайменше в тестовому середовищі. Не створюйте штучні production-помилки без узгодження з командою.
Якщо ініціалізація відбувається після запуску частини застосунку, деякі помилки можуть не бути перехоплені.
Ініціалізуйте SDK на початку головного файлу.
Це створює шум і призводить до ігнорування сповіщень.
Використовуйте warning, error і fatal відповідно до наслідків проблеми.
Не передавайте в Sentry паролі, токени, cookie та повні об’єкти запитів без перевірки.
Формуйте невеликий структурований контекст із безпечних полів.
Fingerprint на кшталт ["error", requestId] створить окрему групу для кожної події.
Власний fingerprint має містити стабільні характеристики логічної проблеми.
Без правильного environment помилки з локальної розробки, staging і production важко розрізняти.
Передавайте середовище через змінну конфігурації та перевіряйте його в панелі моніторингу.
SDK може буферизувати події перед відправленням. Якщо процес одразу завершується, остання подія може не встигнути надіслатися.
Під час коректного завершення процесу дочекайтеся завершення роботи SDK за допомогою Sentry.close().
SDK моніторингу потрібно ініціалізувати на початку запуску Node.js-застосунку.
captureException надсилає винятки разом зі стеком викликів.
Теги призначені для пошуку та фільтрації.
Контекст допомагає зрозуміти стан операції під час помилки.
Дані користувача та запитів потрібно обмежувати й очищати від секретів.
Стандартне групування за стеком зазвичай достатнє.
Власний fingerprint потрібен лише для контрольованого об’єднання логічно однакових помилок.
Сповіщення варто налаштовувати для production і критичних подій, а не для кожного запису.
Перед запуском перевірте групування, контекст, маршрути сповіщень і відсутність чутливих даних.