Подключение Gotcha к 1С-Битрикс: ошибки, производительность и Web Vitals
Gotcha принимает данные по протоколу приёма Sentry, поэтому подключение любого PHP-приложения — это официальный Sentry SDK, нацеленный на ваш инстанс. С Битриксом есть нюанс: у него собственный обработчик ошибок, собственная система модулей и своё представление о том, что такое «страница». Если подключать SDK в лоб, половина ошибок не долетит, а отчёт по производительности окажется бесполезным.
Разберём модуль, который делает всё правильно: ошибки PHP через штатную
точку расширения ядра, транзакции по физическим скриптам, браузерный SDK
через Asset. Готовый архив —
gotcha.monitoring-1.0.0.zip,
проверен на «Управление сайтом: Старт» 26.150.
Что будем собирать
| Сигнал | Откуда шлётся | Что нужно |
|---|---|---|
| Ошибки PHP (исключения и фаталы) | ядро Битрикса | ExceptionHandlerLog в .settings.php |
| Время ответа по скриптам | модуль | транзакции на событиях OnPageStart/OnAfterEpilog |
| Ошибки JS и Web Vitals | браузер | @sentry/browser через Asset |
| Аптайм и SSL | сам Gotcha наружу | HTTP-монитор, кода не требует |
| Алерты | Gotcha | канал (Telegram/webhook/email) в UI |
Где взять DSN
После создания проекта Gotcha перекидывает на страницу «Подключение»
(/projects/<id>/setup). DSN выглядит так:
https://<public_key>@gotcha.example.com/<project_id>
Один и тот же DSN используют и PHP, и браузер — public_key в нём публичен по
замыслу. В модуле он станет настройкой: пустое значение — модуль полностью
выключен.
Главное: Битрикс не отдаст вам ошибки просто так
Первое, что делают при подключении Sentry к любому PHP-проекту, — вызывают
\Sentry\init(), а SDK ставит свои set_error_handler и
set_exception_handler. В Битриксе это не сработает: ядро ставит свой
обработчик в Bitrix\Main\Application::initializeExceptionHandler(), и он
перехватывает всё раньше. Ошибка уйдёт в журнал Битрикса, а до вашего
обработчика не доедет.
Бороться с ядром не нужно — у него есть штатная точка расширения. Битрикс
отдаёт каждую перехваченную ошибку «логу», класс которого задан в
.settings.php:
'exception_handling' => [
'value' => [
'log' => [
'class_name' => '\\Gotcha\\Monitoring\\ExceptionLog',
'required_file' => 'modules/gotcha.monitoring/lib/ExceptionLog.php',
'settings' => [],
],
],
],
required_file резолвится через Loader::getLocal(), то есть путь ищется
сначала в /local/, потом в /bitrix/ — модулю из /local/modules/ этого
достаточно. Класс должен наследовать ExceptionHandlerLog:
namespace Gotcha\Monitoring;
use Bitrix\Main\Diag\ExceptionHandlerLog;
final class ExceptionLog extends ExceptionHandlerLog
{
public function initialize(array $options): void
{
}
public function write($exception, $logType): void
{
if (!$exception instanceof \Throwable || !Sdk::isEnabled()) {
return;
}
// IGNORED_ERROR — то, что ядро само решило не считать проблемой
// (подавленные @-оператором и т.п.). Шлём только настоящие сбои.
if ($logType === self::IGNORED_ERROR || $logType === self::LOW_PRIORITY_ERROR) {
return;
}
\Sentry\withScope(function (\Sentry\State\Scope $scope) use ($exception, $logType): void {
$scope->setTag('bitrix.log_type', self::logTypeToString($logType));
\Sentry\captureException($exception);
});
// Фатальную ошибку процесс не переживёт: без flush событие останется
// в буфере и не уйдёт по сети.
if ($logType === self::FATAL) {
$client = \Sentry\SentrySdk::getCurrentHub()->getClient();
if ($client !== null) {
$client->flush(2);
}
}
}
}
Два места, которые стоит объяснить.
Фильтр по $logType. Битрикс различает виды ошибок: UNCAUGHT_EXCEPTION,
CAUGHT_EXCEPTION, FATAL, но также IGNORED_ERROR (подавленные) и
LOW_PRIORITY_ERROR. Последние два — не сбои, и если их не отсечь, в issues
польётся шум, из-за которого настоящие ошибки потеряются.
flush() на фатале. Sentry SDK копит события в буфере и отправляет их в
конце запроса. При фатальной ошибке «конца запроса» в обычном смысле не будет,
поэтому именно фатал — тот случай, когда буфер надо вытолкнуть руками. Без
этой строчки самые важные ошибки как раз и не долетают.
Прописывать конфиг руками пользователю не нужно: это делает установщик модуля, о нём ниже.
Структура модуля
Модуль для Битрикса — это папка вида вендор.модуль в /local/modules/:
local/modules/gotcha.monitoring/
├── include.php # автозагрузка классов, инициализация SDK
├── options.php # страница настроек в админке
├── install/index.php # класс установщика (CModule)
├── install/version.php
├── lib/Sdk.php # инициализация Sentry SDK
├── lib/ExceptionLog.php # приёмник ошибок ядра
├── lib/Monitor.php # транзакции и браузерный SDK
├── lang/{ru,en}/ # языковые файлы
├── media/js/sentry.min.js # браузерный SDK
└── vendor/ # composer require sentry/sentry
/local/ вместо /bitrix/ — принципиально: содержимое /bitrix/modules/
перетирается обновлениями продукта, а /local/ Битрикс не трогает.
Точка входа
include.php подключается ядром при Loader::includeModule('gotcha.monitoring'),
в том числе автоматически перед вызовом наших обработчиков событий:
use Bitrix\Main\Config\Option;
use Bitrix\Main\Loader;
defined('B_PROLOG_INCLUDED') && B_PROLOG_INCLUDED === true || die();
Loader::registerAutoLoadClasses('gotcha.monitoring', [
'Gotcha\\Monitoring\\Monitor' => 'lib/Monitor.php',
'Gotcha\\Monitoring\\ExceptionLog' => 'lib/ExceptionLog.php',
'Gotcha\\Monitoring\\Sdk' => 'lib/Sdk.php',
]);
Gotcha\Monitoring\Sdk::init();
if (Option::get('gotcha.monitoring', 'browser', 'Y') === 'Y') {
Gotcha\Monitoring\Monitor::injectBrowserSdk();
}
Строка defined('B_PROLOG_INCLUDED') || die() обязательна в каждом файле
модуля: она не даёт выполнить файл при прямом обращении по URL.
Инициализация SDK
\Sentry\init([
'dsn' => $dsn,
'environment' => (string) Option::get('gotcha.monitoring', 'environment', 'production'),
'release' => 'bitrix@' . (defined('SM_VERSION') ? SM_VERSION : 'unknown'),
'traces_sample_rate' => (float) Option::get('gotcha.monitoring', 'traces_sample_rate', '0.2'),
// Обработчики ставит Битрикс: свои повесит ExceptionLog, а этим
// мы бы только перебили штатную обработку ошибок ядра.
'error_types' => 0,
]);
error_types => 0 — важная деталь. Ошибки к нам приходят через
ExceptionLog, а обработчики самого SDK тут только мешали бы: в лучшем случае
дублировали события, в худшем — ломали штатную обработку ошибок Битрикса.
release берём из SM_VERSION, чтобы в Gotcha было видно, на какой версии
продукта случилась регрессия.
Имя транзакции: физический скрипт, а не URL
В Битриксе нет роутов в привычном смысле. Есть ЧПУ, которое urlrewrite
разворачивает в один и тот же файл: /catalog/tovar-123/ и
/catalog/tovar-456/ исполняет /catalog/index.php. Он и есть эндпойнт.
Возьми мы URL — и у магазина с двадцатью тысячами товаров появится двадцать тысяч «эндпойнтов», по одному на карточку, а раздел «Производительность» перестанет что-либо значить (подробнее — Кардинальность).
private static function transactionName(): string
{
if (self::isConsole()) {
return 'cron';
}
$script = (string) ($_SERVER['SCRIPT_NAME'] ?? '');
if ($script === '') {
return 'unknown';
}
// Общая точка входа AJAX-компонентов: без действия все запросы
// слиплись бы в один эндпойнт.
if (str_ends_with($script, '/bitrix/services/main/ajax.php')) {
$action = (string) ($_REQUEST['action'] ?? '');
return $action !== '' ? 'ajax:' . $action : 'ajax';
}
return $script;
}
Получается ровно то, что нужно видеть: /index.php, /catalog/index.php,
/bitrix/admin/user_edit.php, ajax:..., cron. Десятки строк вместо
десятков тысяч.
Транзакция: события ядра плюс страховка
public static function onPageStart(): void
{
if (!Sdk::isEnabled() || self::$transaction !== null) {
return;
}
$context = TransactionContext::make()
->setName(self::transactionName())
->setOp(self::isConsole() ? 'console' : 'http.server');
self::$transaction = \Sentry\startTransaction($context);
SentrySdk::getCurrentHub()->setSpan(self::$transaction);
// Не только OnAfterEpilog: при фатальной ошибке до событий ядра дело
// не доходит, а shutdown-функция PHP вызывается всегда.
register_shutdown_function([self::class, 'finish']);
}
OnPageStart — самое раннее событие ядра, до пролога. Закрываем транзакцию на
OnAfterEpilog, но дублируем register_shutdown_function: при фатальной
ошибке эпилог не выполняется, а shutdown-обработчик PHP отработает всегда.
Браузерный SDK
У Битрикса штатный менеджер ассетов, ему и отдаём скрипт:
$asset = Asset::getInstance();
$asset->addJs('/bitrix/js/gotcha.monitoring/sentry.min.js');
$asset->addString(
'<script>Sentry.init({'
. 'dsn: ' . json_encode($dsn) . ','
. 'integrations: [Sentry.browserTracingIntegration()],'
. 'tracesSampleRate: ' . $rate
. '});</script>'
);
Всё это попадёт в страницу при вызове $APPLICATION->ShowHead() — стандартной
строке любого шаблона Битрикса. browserTracingIntegration сам снимает Web
Vitals (LCP, CLS, INP, FCP, TTFB) и ловит JS-ошибки.
Бандл собирается один раз любым бандлером:
npm i @sentry/browser esbuild
npx esbuild <(echo "export * from '@sentry/browser'") \
--bundle --minify --format=iife --global-name=Sentry \
--outfile=media/js/sentry.min.js
Установщик
Класс установщика в Битриксе называется по имени модуля с точкой, заменённой на
подчёркивание, и наследует CModule. Он регистрирует модуль, подписывается на
события и — главное — сам прописывает наш класс в .settings.php:
public function DoInstall(): bool
{
ModuleManager::registerModule($this->MODULE_ID);
RegisterModuleDependences('main', 'OnPageStart', $this->MODULE_ID,
'\Gotcha\Monitoring\Monitor', 'onPageStart');
RegisterModuleDependences('main', 'OnAfterEpilog', $this->MODULE_ID,
'\Gotcha\Monitoring\Monitor', 'onAfterEpilog');
$this->copyAssets();
$this->registerExceptionLog();
return true;
}
private function registerExceptionLog(): void
{
$configuration = Configuration::getInstance();
$value = $configuration->get('exception_handling') ?? [];
$value['log'] = [
'class_name' => '\\Gotcha\\Monitoring\\ExceptionLog',
'required_file' => 'modules/gotcha.monitoring/lib/ExceptionLog.php',
'settings' => [],
];
$configuration->setValue('exception_handling', $value);
$configuration->saveConfiguration();
}
DoUninstall() делает обратное: убирает log из конфига, удаляет js-файл и
снимает подписки. Модуль, который не умеет удаляться начисто, — плохой модуль.
Грабля из практики.
CModuleобъявляет методыInstallFiles()иUnInstallFiles()публичными. Если назвать свой приватный методinstallFiles(), PHP выдаст фатальную ошибку прямо при установке: имена методов регистронезависимы, и приватный метод не может переопределить публичный. Называйте свои методы иначе — напримерcopyAssets().
Настройки
options.php в корне модуля Битрикс сам показывает в разделе Настройки →
Настройки продукта → Настройки модулей. Страница делается штатным
CAdminTabControl, значения хранятся в Option::set()/Option::get().
Обязательное: проверка $USER->IsAdmin() и check_bitrix_sessid() перед
сохранением — без неё форма открыта для CSRF.
Обязательный шаг: изоляция vendor
Модуль несёт свой vendor/, и соседние модули — тоже. Там встречаются те же
пакеты (psr/log, guzzlehttp/*, symfony/*) других мажорных версий.
Побеждает автолоадер, сработавший первым, а дальше — fatal error на ровном
месте:
PHP Fatal error: Declaration of Psr\Log\NullLogger::log($level, Stringable|string $message,
array $context = []): void must be compatible with Psr\Log\LoggerInterface::log($level,
$message, array $context = [])
Ровно это мы поймали, делая такой же плагин для Joomla: на новых версиях всё работало, а на старой сайт отдавал белый экран на каждой странице.
Лечится переименованием namespace зависимостей при сборке — php-scoper:
// scoper.inc.php
return [
'prefix' => 'Gotcha\\Monitoring\\Vendor',
'finders' => [
Finder::create()->files()->in('vendor'),
Finder::create()->files()->name('*.php')->in('lib'),
],
// Namespace модуля и классы ядра Битрикса не трогаем.
'exclude-namespaces' => ['Gotcha\\Monitoring', 'Bitrix'],
// API Битрикса — глобальные функции и классы (CModule, CAdminTabControl).
'expose-global-functions' => true,
'expose-global-classes' => true,
];
Для Битрикса важны две последние строки: половина его API — глобальные функции
(RegisterModuleDependences, CopyDirFiles) и классы без namespace
(CModule, CAdminTabControl). Переименуй их — и модуль не установится.
Установка
unzip gotcha.monitoring-1.0.0.zip -d /path/to/site/local/modules/
Затем Marketplace → Установленные решения, найти «Gotcha: мониторинг ошибок и производительности», нажать «Установить». После этого — Настройки → Настройки продукта → Настройки модулей → Gotcha, вставить DSN.
Проверка: бросьте исключение в любом файле сайта — событие появится в разделе «Проблемы» через несколько секунд.
Аптайм и алерты
Аптайм не требует правок в коде — Gotcha сам ходит на публичный URL снаружи: Аптайм → Новый монитор → HTTP, URL сайта, интервал и пороги. Инциденты, SSL-алерты и публичная status-страница — из коробки (Аптайм).
В разделе «Оповещения» правила (новый issue, регрессия, всплеск) уже включены; остаётся добавить канал доставки — Telegram, webhook или email (Алерты).
Что смотреть на сайте под Битриксом в первую очередь
| Метрика | Зачем |
|---|---|
| Ошибки PHP по релизам | «после обновления модуля посыпались 500-е» |
| Ошибки JS в браузере | корзина или фильтр сломаны у части посетителей |
| p95 по скриптам | видно, что медленно: каталог, оформление заказа, поиск |
| Web Vitals (LCP, INP, CLS) | реальная скорость у людей, а не в Lighthouse |
Время ajax:* и агентов | классический источник тормозов интернет-магазина |
| Аптайм и срок SSL | сайт лежит / сертификат истекает через три дня |
Итог
- Ошибки — через
ExceptionHandlerLog, а неset_exception_handler: у ядра Битрикса свой обработчик, и он всегда первый. flush()на фатале — иначе самые важные события остаются в буфере.- Имя транзакции — физический скрипт, а не URL: под ЧПУ иначе каждый товар станет отдельным эндпойнтом.
register_shutdown_functionв дополнение кOnAfterEpilog— при фатале эпилог не выполняется.- Модуль в
/local/modules/, чтобы его не стёрло обновлением продукта. vendor/обязательно скоупить, но сexpose-global-functions— половина API Битрикса глобальная.
Дальше — документация, установка и раздел подключения SDK.