Привет! Меня зовут Даниил Ткаченко, я веб‑разработчик в ИТ‑компании «Активика». В статье я поделюсь опытом развёртывания Sentry self‑hosted для высоконагруженного проекта. Несмотря на обилие материалов по SaaS‑версии, актуальных гайдов по self‑hosted‑установке почти нет — особенно с учётом современных требований к производительности и отказоустойчивости.
Мы столкнулись с рядом проблем: нестабильностью на базовом хостинге, отсутствием перехвата HTTP‑ошибок и быстрым заполнением диска. Под катом разберу каждую проблему, покажу код решений и дам рекомендации для тех, кто планирует развернуть Sentry самостоятельно.
Статья будет полезна разработчикам и DevOps‑инженерам без опыта работы с self‑hosted Sentry.
Проблема 1. Нестабильность на базовом хостинге
Всё началось с того, что на нашем базовом хостинге возникли ошибки: Sentry стабильно не запускался из‑за особенностей виртуализации. Нельзя было даже развернуть инструмент. Поэтому мы переехали на другой хостинг — и тогда всё получилось.
Но тут меня тоже поджидали проблемы: память быстро забивалась, и не просто логами в том же ClickHouse или файлами реплеев, а логами в Kafka.
За время тестовых подключений к демоверсии проекта (а ей пользуется ограниченное число пользователей для тестирования новых функций) Sentry падал два раза, а демо-версия проекта грузилась очень долго.
Когда Sentry падал, сайт грузился очень долго — около 30–40 секунд. Это происходило потому, что клиент пытался не только загрузить JS‑скрипты с внешних CDN, но и отправить события на сервер Sentry. Запросы зависали на таймаутах, блокируя загрузку страницы.
Конфигурация сервера, которую использовали: 32 ГБ RAM, 4 ядра CPU, 40 ГБ SSD. Установили до ажиотажа с ценами на память — потом пришлось уменьшить RAM до 16-24 ГБ, увеличить SSD до 100ГБ и добавить swap-файл, чтобы не переплачивать.
Лайфхаки для ускорения загрузки сайта, когда сентри не доступен
Даже при self‑hosted решении возможны сбои: DDoS‑атаки, перезагрузка кластера, ошибки ПО. Делюсь проверенными мной способами минимизировать их влияние, чтобы избежать длительной загрузки и работы сайта, даже при полном отказе Sentry.
Хак 1. Локальная урезанная версия JS‑библиотеки
Я скачал JS-библиотеку Sentry и хранил её локально в урезанной версии. Теперь никаких внешних запросов — загрузка мгновенная и без зависаний.
Хак 2. Таймауты на соединения
Добавляем код с таймаутами на соединения поверх SDK, который прерывает запросы к Sentry, если они длятся дольше 300 миллисекунд.
Это предотвращает переполнение очереди запросов в браузере и сохраняет отзывчивость сайта.
const SENTRY_TIMEOUT_MS = 300; function makeCustomTimeoutTransport(options) { // The 'makeRequest' function is the core where you control the fetch call. function makeRequest(request) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), SENTRY_TIMEOUT_MS); // 3-second timeout const requestOptions = { body: request.body, method: 'POST', referrerPolicy: 'origin', headers: options.headers, signal: controller.signal, // <-- Connect the AbortController ...options.fetchOptions, }; // Return the fetch promise. It will be rejected if it times out. return fetch(options.url, requestOptions) .then(response => { clearTimeout(timeoutId); // Clear timer on success return { statusCode: response.status, headers: { 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), 'retry-after': response.headers.get('Retry-After'), }, }; }) .catch(error => { clearTimeout(timeoutId); if (error.name === 'AbortError') { console.warn('Sentry event dropped due to send timeout.'); error.message = 'Sentry event dropped due to send timeout.'; } throw error; // Re-throw to let the SDK handle the failure }); } // Use the official helper to build a compliant transport. return Sentry.createTransport(options, makeRequest); } Sentry.init({ transport: makeCustomTimeoutTransport,… });
Эти изменения мы протестировали на проде.
Результат: после внедрения перестало забиваться лог бесконечными попытками достучаться до Sentry.
Проблема 2. Отсутствие перехвата HTTP‑ошибок
Оказалось, что Sentry из коробки не ловит HTTP-ошибки на асинхронных запросах — картинки 404, сломанные API-вызовы или страницы проходят мимо. Я написал небольшое расширение, примерно на 100 строк JS, которое:
-
перехватывает запросы через fetch и XMLHttpRequest
-
отслеживает ошибки (коды 4xx, 5xx)
-
отправляет информацию в Sentry с деталями запроса и ответа
Ключевые возможности расширения:
-
фильтрация по URL и кодам статусов
-
ограничение размера данных (MAX_CAPTURE_LENGTH = 1000 байт)
-
санитизация небезопасных заголовков (cookie, authorization и т. д.)
-
поддержка PII (персональных данных) при необходимости
// sentry-http-integration.js/** * Расширенная интеграция для перехвата HTTP-запросов и отправки информации об ошибках в Sentry. * Основана на официальной реализации httpClientIntegration из Sentry, но с дополнительными возможностями * для более детального анализа запросов и ответов. * * @see Официальная документация Sentry: https://docs.sentry.io/platforms/javascript/ */class SentryHttpDataIntegration { /** * Уникальный идентификатор интеграции. * @type {string} */ static id = 'SentryHttpDataIntegration'; /** * Имя интеграции. * @type {string} */ name = 'SentryHttpDataIntegration'; /** * Максимальная длина захватываемых данных в байтах. * Используется для предотвращения отправки слишком больших данных в Sentry. * @type {number} */ static MAX_CAPTURE_LENGTH = 1000; /** * Список "небезопасных" заголовков, доступ к которым ограничен в браузерах. * @type {Array<string>} * @private */ static _RESTRICTED_HEADERS = [ 'set-cookie', 'set-cookie2', 'cookie2', 'cookie', 'authorization', 'proxy-authorization', 'sec-', 'proxy-' ]; /** * Настройки для фильтрации HTTP-запросов, по которым будут отправляться события. * @type {Object} * @property {Array<Array<number>|number>} failedRequestStatusCodes - Коды HTTP-статусов, которые считаются ошибками * @property {Array<string|RegExp>} failedRequestTargets - URL-шаблоны для отслеживания ошибок * @private */ _options = { failedRequestStatusCodes: [[500, 599]], failedRequestTargets: [/.*/], }; /** * Конструктор класса. * @param {Object} options - Настройки интеграции * @param {Array<Array<number>|number>} [options.failedRequestStatusCodes] - Коды HTTP-статусов для отслеживания * @param {Array<string|RegExp>} [options.failedRequestTargets] - URL-шаблоны для отслеживания */ constructor(options = {}) { this._options = { ...this._options, ...options, }; } /** * Устанавливает перехватчики для fetch и XMLHttpRequest. * Этот метод вызывается Sentry при инициализации интеграции. */ setupOnce() { this._wrapFetch(); this._wrapXHR(); } /** * Проверяет, должен ли запрос быть обработан на основе его URL и статуса ответа. * @param {number} status - Код HTTP-статуса * @param {string} url - URL запроса * @returns {boolean} - true, если запрос соответствует критериям для отправки в Sentry * @private */ _shouldCaptureResponse(status, url) { // Не обрабатываем запросы к самому Sentry if (this._isSentryRequest(url)) { return false; } return ( this._isInStatusCodeRange(status) && this._isUrlInTargets(url) ); } /** * Проверяет, соответствует ли код статуса заданным диапазонам ошибок. * @param {number} status - Код HTTP-статуса * @returns {boolean} - true, если статус входит в диапазоны ошибок * @private */ _isInStatusCodeRange(status) { return this._options.failedRequestStatusCodes.some(range => { if (typeof range === 'number') { return status === range; } return status >= range[0] && status <= range[1]; }); } /** * Проверяет, соответствует ли URL заданным шаблонам. * @param {string} url - URL запроса * @returns {boolean} - true, если URL соответствует хотя бы одному шаблону * @private */ _isUrlInTargets(url) { return this._options.failedRequestTargets.some(target => { if (typeof target === 'string') { return url.includes(target); } return target.test(url); }); } /** * Проверяет, является ли запрос запросом к Sentry. * @param {string} url - URL запроса * @returns {boolean} - true, если запрос направлен к Sentry * @private */ _isSentryRequest(url) { // Простая проверка на запросы к Sentry // В реальной реализации может использоваться isSentryRequestUrl из @sentry/core return url.includes('sentry.io') || url.includes('ingest.sentry.io'); } /** * Перехватывает нативный fetch API для отслеживания запросов. * Заменяет глобальную функцию fetch на свою обертку, которая: * - сохраняет информацию о запросе * - выполняет оригинальный запрос * - перехватывает ошибки и ответы с кодом ошибки (4xx, 5xx) * - отправляет информацию в Sentry в случае ошибки * @private */ _wrapFetch() { if (typeof window.fetch !== 'function') { console.warn('Fetch API не поддерживается в этом окружении'); return; } const originalFetch = window.fetch; const self = this; window.fetch = async function (input, init) { // Получаем метод запроса (GET по умолчанию) const method = init?.method ?? 'GET'; // Создаем объект Request для унификации работы с разными форматами параметров const request = self._getRequest(input, init); // Захватываем данные запроса, если они есть const requestData = init?.body ? await self._captureRequestBody(init.body) : null; // Извлекаем заголовки запроса, если sendDefaultPii активен let requestHeaders; if (self._shouldSendDefaultPii()) { try { requestHeaders = self._extractFetchHeaders(request.headers); // Удаляем небезопасные заголовки self._sanitizeHeaders(requestHeaders); } catch (e) { // Игнорируем ошибки при извлечении заголовков } } try { // Выполняем оригинальный запрос const response = await originalFetch.apply(this, arguments); // Если ответ соответствует критериям ошибки if (self._shouldCaptureResponse(response.status, response.url)) { // Извлекаем заголовки ответа, если активен sendDefaultPii let responseHeaders; if (self._shouldSendDefaultPii()) { try { responseHeaders = self._extractFetchHeaders(response.headers); // Удаляем небезопасные заголовки self._sanitizeHeaders(responseHeaders); } catch (e) { // Игнорируем ошибки при извлечении заголовков } } // Захватываем тело ответа const responseData = await self._captureResponseBody(response.clone()); // Отправляем информацию об ошибке в Sentry self._captureError({ method, url: response.url, requestData, requestHeaders, responseData, responseHeaders, status: response.status }); } return response; } catch (error) { // В случае сетевой ошибки отправляем информацию в Sentry self._captureError({ method, url: request.url, requestData, requestHeaders, error: error.message, status: '' }); // Передаем ошибку дальше throw error; } }; } /** * Создает объект Request из параметров fetch. * @param {RequestInfo} input - URL или объект Request * @param {RequestInit} [init] - Опции запроса * @returns {Request} - Объект Request * @private */ _getRequest(input, init) { if (!init && input instanceof Request) { return input; } // Если тело оригинального Request уже использовано, просто возвращаем его if (input instanceof Request && input.bodyUsed) { return input; } return new Request(input, init); } /** * Перехватывает XMLHttpRequest для отслеживания запросов. * Модифицирует прототип XMLHttpRequest, заменяя методы open и send * для сбора информации о запросах и перехвата ошибок. * @private */ _wrapXHR() { if (typeof XMLHttpRequest === 'undefined') { console.warn('XMLHttpRequest не поддерживается в этом окружении'); return; } const originalSend = XMLHttpRequest.prototype.send; const originalOpen = XMLHttpRequest.prototype.open; const self = this; // Сохраняем метод и URL запроса через перехват open XMLHttpRequest.prototype.open = function (method, url) { this._method = method; this._url = url; this._requestHeaders = {}; return originalOpen.apply(this, arguments); }; // Перехватываем setRequestHeader для сохранения заголовков const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.setRequestHeader = function (name, value) { if (!this._requestHeaders) { this._requestHeaders = {}; } this._requestHeaders[name] = value; return originalSetRequestHeader.apply(this, arguments); }; // Перехватываем метод send для отслеживания запросов XMLHttpRequest.prototype.send = function (body) { // Захватываем данные запроса const requestData = self._captureSyncData(body); // Санитизируем заголовки запроса let requestHeaders; if (self._shouldSendDefaultPii() && this._requestHeaders) { requestHeaders = {...this._requestHeaders}; self._sanitizeHeaders(requestHeaders); } // Добавляем обработчик события завершения запроса this.addEventListener('loadend', () => { // Если статус ответа указывает на ошибку и URL соответствует критериям if (self._shouldCaptureResponse(this.status, this.responseURL ?? this._url)) { // Безопасно получаем заголовки ответа let responseHeaders; if (self._shouldSendDefaultPii()) { try { // Используем безопасный метод получения заголовков ответа responseHeaders = self._getXHRResponseHeaders(this); // Дополнительно санитизируем заголовки self._sanitizeHeaders(responseHeaders); } catch (e) { // Игнорируем ошибки при извлечении заголовков } } // Захватываем тело ответа const responseData = self._captureSyncXHRResponseData(this); // Отправляем информацию об ошибке в Sentry self._captureError({ method: this._method ?? 'GET', url: this.responseURL ?? this._url, requestData, requestHeaders, responseData, responseHeaders, status: this.status }); } }); // Вызываем оригинальный метод send return originalSend.call(this, body); }; } /** * Удаляет небезопасные заголовки из объекта заголовков. * @param {Object} headers - Объект с заголовками * @private */ _sanitizeHeaders(headers) { if (!headers) return; // Удаляем небезопасные заголовки Object.keys(headers).forEach(key => { const lowerKey = key.toLowerCase(); if (SentryHttpDataIntegration._RESTRICTED_HEADERS.some( restrictedHeader => lowerKey === restrictedHeader || lowerKey.startsWith(restrictedHeader) )) { delete headers[key]; } }); } /** * Безопасно захватывает тело ответа XHR. * @param {XMLHttpRequest} xhr - Объект XMLHttpRequest * @returns {string|Object|null} - Захваченные данные ответа * @private */ _captureSyncXHRResponseData(xhr) { try { // Безопасно получаем тело ответа, обрабатывая разные типы ответов let responseData = xhr.response; // Проверяем тип ответа и при необходимости конвертируем if (responseData) { if (typeof responseData === 'object') { try { // Для объектов попробуем преобразовать их в строку JSON responseData = JSON.stringify(responseData).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH); } catch (e) { // Если не удалось преобразовать, вернем тип объекта responseData = `[${Object.prototype.toString.call(responseData)}]`; } } else if (typeof responseData === 'string') { // Ограничиваем длину строки responseData = responseData.slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH); } else { // Для других типов данных преобразуем их в строку responseData = String(responseData).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH); } } return responseData; } catch (e) { return '[unable to capture response data]'; } } /** * Проверяет, нужно ли отправлять персональные данные (PII). * @returns {boolean} - true, если нужно отправлять PII * @private */ _shouldSendDefaultPii() { // В реальной реализации это зависит от настроек Sentry // Здесь просто возвращаем true для демонстрации return true; } /** * Извлекает заголовки из объекта Headers. * @param {Headers} headers - Объект Headers * @returns {Object} - Объект с заголовками * @private */ _extractFetchHeaders(headers) { const result = {}; if (headers instanceof Headers) { headers.forEach((value, key) => { // Проверяем, не является ли заголовок небезопасным if (!SentryHttpDataIntegration._RESTRICTED_HEADERS.some( restrictedHeader => key.toLowerCase() === restrictedHeader || key.toLowerCase().startsWith(restrictedHeader) )) { result[key] = value; } }); } return result; } /** * Извлекает заголовки ответа из XMLHttpRequest. * @param {XMLHttpRequest} xhr - Объект XMLHttpRequest * @returns {Object} - Объект с заголовками * @private */ _getXHRResponseHeaders(xhr) { const headers = xhr.getAllResponseHeaders(); const result = {}; if (!headers) { return result; } headers.split('\r\n').forEach(line => { if (!line) return; const separatorIndex = line.indexOf(': '); if (separatorIndex > 0) { const key = line.substring(0, separatorIndex); const value = line.substring(separatorIndex + 2); // Проверяем, не является ли заголовок небезопасным if (!SentryHttpDataIntegration._RESTRICTED_HEADERS.some( restrictedHeader => key.toLowerCase() === restrictedHeader || key.toLowerCase().startsWith(restrictedHeader) )) { result[key] = value; } } }); return result; } /** * Вспомогательные методы для захвата данных */ /** * Захватывает и обрабатывает тело запроса для последующей отправки в Sentry. * Поддерживает различные типы данных: FormData, Blob, Response и строки. * @param {*} body - Тело запроса * @returns {Promise<Object|string|null>} - Обработанные данные запроса * @private */ async _captureRequestBody(body) { try { // Обработка данных FormData if (body instanceof FormData) { return Object.fromEntries(body.entries()); } // Обработка объектов, поддерживающих метод text() (Request, Response, Blob) if (typeof body.text === 'function') { const text = await body.text(); return text.slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH); } // Обработка остальных типов данных return String(body).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH); } catch { // В случае ошибки возвращаем информативную строку return '[unable to capture request body]'; } } /** * Захватывает и обрабатывает тело ответа для последующей отправки в Sentry. * @param {Response} response - Объект Response * @returns {Promise<string>} - Текстовое представление тела ответа * @private */ async _captureResponseBody(response) { try { // Пытаемся получить тело как текст const text = await response.text(); return text.slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH); } catch (e) { try { // Если не удалось получить как текст, пробуем работать с типом ответа const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { // Для JSON пытаемся получить и преобразовать данные const clone = response.clone(); const json = await clone.json(); return JSON.stringify(json).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH); } if (contentType && contentType.includes('text/')) { // Для текстовых форматов пытаемся еще раз получить текст const clone = response.clone(); return (await clone.text()).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH); } // Для бинарных данных возвращаем информацию о типе return `[binary data: ${contentType ?? 'unknown type'}]`; } catch { return '[unable to capture response body]'; } } } /** * Синхронно захватывает данные для XMLHttpRequest запросов. * @param {*} data - Данные для захвата * @returns {Object|string|null} - Обработанные данные * @private */ _captureSyncData(data) { try { if (!data) return null; // Обработка данных FormData if (data instanceof FormData) { return Object.fromEntries(data.entries()); } // Обработка данных Blob if (data instanceof Blob) { return `[Blob data: ${data.type ?? 'unknown type'}, size: ${data.size} bytes]`; } // Обработка данных ArrayBuffer if (data instanceof ArrayBuffer) { return `[ArrayBuffer data: size: ${data.byteLength} bytes]`; } // Если данные - объект, пытаемся преобразовать в JSON if (typeof data === 'object' && data !== null) { try { return JSON.stringify(data).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH); } catch (e) { return `[object: ${Object.prototype.toString.call(data)}]`; } } // Обработка остальных типов данных return String(data).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH); } catch (e) { return '[unable to capture data]'; } } /** * Получает размер ответа из заголовка Content-Length. * @param {Object} headers - Заголовки ответа * @returns {number|undefined} - Размер ответа в байтах или undefined * @private */ _getResponseSizeFromHeaders(headers) { if (!headers) return undefined; const contentLength = headers['Content-Length'] ?? headers['content-length']; if (contentLength) { return parseInt(contentLength, 10); } return undefined; } /** * Формирует и отправляет информацию об ошибке в Sentry. * @param {Object} details - Детали ошибки * @param {string} details.method - HTTP метод запроса * @param {string} details.url - URL запроса * @param {*} details.requestData - Данные запроса * @param {Object} [details.requestHeaders] - Заголовки запроса * @param {number} [details.status] - Статус ответа (если есть) * @param {*} [details.responseData] - Данные ответа (если есть) * @param {Object} [details.responseHeaders] - Заголовки ответа * @param {string} [details.error] - Сообщение об ошибке (если есть) * @private */ _captureError(details) { // Создаем объект ошибки с сообщением из details.error или на основе статуса ответа const error = new Error(details.error ?? `HTTP Error ${details.status ?? ''}`); // Формируем сообщение об ошибке const message = `HTTP Client Error: ${details.method} ${details.url} ${details.status ?? ''}`; // Формируем событие для Sentry const event = { message, exception: { values: [ { type: 'Error', value: message, }, ], }, request: { url: details.url, method: details.method, headers: details.requestHeaders, data: details.requestData, }, contexts: { response: { status_code: details.status, headers: details.responseHeaders ?? {}, data: details.responseData ?? {}, body_size: this._getResponseSizeFromHeaders(details.responseHeaders), }, }, }; // Добавляем механизм исключения this._addExceptionMechanism(event); // Отправляем событие в Sentry Sentry.captureException(error, { contexts: event.contexts, request: event.request, tags: { 'http.status_code': details.status ?? '', 'http.method': details.method, }, }); } /** * Добавляет механизм исключения к событию. * @param {Object} event - Событие Sentry * @private */ _addExceptionMechanism(event) { if (event.exception && event.exception.values && event.exception.values[0]) { event.exception.values[0].mechanism = { type: 'http.client', handled: false, }; } }}/** * Универсальный экспорт класса интеграции. * Поддерживает как CommonJS модули (Node.js), так и браузерную среду. */if (typeof module !== 'undefined' && module.exports) { module.exports = SentryHttpDataIntegration;} else { window.SentryHttpDataIntegration = SentryHttpDataIntegration;}
Проблема 3. Быстрое заполнение диска
Основная проблема — быстрое заполнение диска. 40 ГБ заполнялись за часы при включённом трейсинге и метриках. Sentry накапливает данные в ClickHouse, Kafka и Postgres.
Если всё-таки Sentry заполнил всё пространство, но повышать объём хранилища вы не хотите, то можно удалить некоторые docker-volume. Например, sentry-kafka, sentry-seaweedfs, возможно даже sentry-data. После их удаления запустите скрипт установки Sentry, и он заново создаст нужные docker volume.
Решения для снижения нагрузки:
-
Снижение срока хранения событий. Установили срок хранения событий в системе до 2-4 дней (в файле .env: SENTRY_EVENT_RETENTION_DAYS). Для хранения 14 дней потребуется 200+ ГБ.
-
Отключение ненужных трейсингов:
-
браузерные спаны
-
ошибки от сторонних CDN (например, таймауты Google Fonts)
-
фоновые задачи (вкладка Performance в настройках проекта)
-
3. Оптимизация Kafka:
-
уменьшили retention событий до минимума — данные обрабатываются и удаляются.
Такие у нас настройки для контейнера Kafka:
KAFKA_LOG_RETENTION_HOURS: «1»
KAFKA_MESSAGE_MAX_BYTES: «10000000» #10MB or bust
KAFKA_MAX_REQUEST_SIZE: «10000000» #10MB on requests apparently too
CONFLUENT_SUPPORT_METRICS_ENABLE: «false»
KAFKA_LOG_RETENTION_BYTES: «10737418240» # 10 GiB (server default; помните про «на партицию»)
KAFKA_LOG_SEGMENT_BYTES: «268435456» # 256 MiB сегмент, чтобы быстрее закрывались и удалялись
KAFKA_LOG_RETENTION_CHECK_INTERVAL_MS: «300000» # каждые 5 минут проверять old segments
KAFKA_LOG_SEGMENT_DELETE_DELAY_MS: «60000» # задержка удаления сегмента
KAFKA_LOG_CLEANUP_POLICY: «delete»
KAFKA_LOG_CLEANER_ENABLE: true
Вот ещё ссылка на доку: https://develop.sentry.dev/self-hosted/troubleshooting/kafka/#reducing-disk-usage
-
Вручную чистили ClickHouse: TRUNCATE на старых таблицах вроде spans_local, transactions_local.
-
Настроили cron для ежедневной очистки файлов, которые хранятся больше нашего срока в системе: find /var/lib/docker/volumes/sentry-data/_data -type f -mtime +3 -delete
Заключение
Развёртывание Sentry self‑hosted — задача нетривиальная, особенно для высоконагруженных проектов. В ходе работы мы:
-
устранили зависания сайта из‑за недоступности Sentry (локальная JS‑библиотека + таймауты);
-
расширили мониторинг ошибок (интеграция для перехвата HTTP‑ошибок);
-
снизили нагрузку на диск (оптимизация Kafka, ClickHouse, настройка retention).
Рекомендации для тех, кто планирует развернуть Sentry self‑hosted:
-
Начинайте с конфигурации, которая соответствует нагрузке (минимум 16 ГБ RAM, 4 ядра CPU).
-
Сразу локализуйте JS‑библиотеку Sentry — это предотвратит зависания сайта.
-
Настройте таймауты для запросов к Sentry — 300–500 мс достаточно для большинства сценариев.
-
Отключайте ненужные трейсинги и метрики — это сэкономит место и ресурсы.
-
Регулярно проверяйте использование диска и настраивайте retention данных.
-
Тестируйте отказоустойчивость — симулируйте сбои Sentry и проверяйте поведение приложения.
Если у вас остались вопросы или есть идеи для улучшения — пишите в комментариях!
ссылка на оригинал статьи https://habr.com/ru/articles/1030358/