Подключение Gotcha к Joomla: ошибки, производительность и метрики сайта
Gotcha принимает данные по протоколу приёма Sentry и по OTLP, поэтому подключение любого PHP-приложения — это официальный Sentry SDK, нацеленный на ваш инстанс. Для Symfony и Laravel есть готовые бандлы, а вот официального пакета «Sentry для Joomla» не существует — и это отличный повод показать, как устроено подключение «руками»: один небольшой системный плагин закрывает ошибки, трейсинг и Web Vitals разом — и работает одинаково на Joomla 4.2+, 5 и 6. Полный код — ниже, а если собирать руками не хочется — есть готовый к установке пакет с тем же кодом (проверен на Joomla 4.4, 5.4 и 6).
Что будем собирать
| Сигнал | Откуда шлётся | Что нужно |
|---|---|---|
| Ошибки PHP (компоненты, плагины, шаблон) | бэкенд Joomla | плагин, событие onError |
| Время ответа по «эндпойнтам» | бэкенд Joomla | плагин, транзакции |
| Ошибки JS и Web Vitals | браузер | @sentry/browser, инъекция тем же плагином |
| Бизнес-метрики (заказы, регистрации) | cron → OTLP | POST на /v1/metrics |
| Аптайм и SSL | сам Gotcha наружу | HTTP-монитор, кода не требует |
| Алерты | Gotcha | канал (Telegram/webhook/email) в UI |
Где взять DSN
После создания проекта Gotcha перекидывает на страницу «Подключение»
(/projects/<id>/setup); вернуться можно кнопкой «Подключение SDK». DSN
выглядит так:
https://<public_key>@gotcha.example.com/<project_id>
Один и тот же DSN используют и PHP, и браузер — public_key в нём публичен по
замыслу. В нашем плагине DSN станет параметром: пустое значение — плагин
полностью выключен, переезд на другой инстанс — правка одного поля в админке.
Почему именно системный плагин
Две особенности Joomla, из-за которых «просто \Sentry\init() в index.php» не
сработает как надо:
- Joomla сама ловит исключения. Необработанное исключение компонента не
доходит до
set_exception_handler— ядро перехватывает его и рисует страницу ошибки. Зато перед этим оно рассылает событиеonError, и плагин может забрать из него исходныйThrowable. - Чистый PHP SDK не создаёт транзакций сам. В отличие от бандлов Symfony и
Laravel,
sentry/sentryбез фреймворк-интеграции не инструментирует HTTP-запросы — транзакцию нужно открыть и закрыть вручную. В Joomla для этого идеально подходят событияonAfterRouteиonAfterRespond.
Системный плагин слушает оба события плюс ещё два — и получается полноценная интеграция без единой правки ядра или шаблона.
Архитектура: пакет из плагина и библиотеки
Соблазнительно положить vendor/ прямо в папку плагина — работать будет. Но
по-джумловому правильнее собрать пакет (pkg_), внутри которого два
расширения: системный плагин с логикой и библиотека (lib_) с
composer-зависимостями.
Зачем так:
- Библиотеку видно в списке расширений. Администратор понимает, что за
папка появилась в
libraries/и откуда она взялась; при удалении пакета Joomla снимет оба расширения. - Зависимости обновляются отдельно от логики. Вышел патч Sentry SDK — обновляется библиотека, плагин не трогается, и наоборот.
- Библиотеку можно переиспользовать. Появится второе расширение Gotcha
(например, модуль статус-панели) — оно возьмёт тот же
libraries/gotcha, а не притащит вторую копию SDK.
pkg_gotcha.zip
├── pkg_gotcha.xml # манифест пакета
├── script.php # проверки версий + сообщение после установки
├── language/{ru-RU,en-GB}/ # строки пакета
└── packages/
├── lib_gotcha.zip # библиотека: vendor + манифест
└── plg_system_gotcha.zip # плагин: логика, языки, браузерный бандл
Манифест пакета перечисляет вложенные расширения — Joomla ставит их сама, по порядку:
<extension type="package" method="upgrade">
<name>PKG_GOTCHA</name>
<packagename>gotcha</packagename>
<version>1.2.1</version>
<scriptfile>script.php</scriptfile>
<files folder="packages">
<file type="library" id="gotcha">lib_gotcha.zip</file>
<file type="plugin" id="gotcha" group="system">plg_system_gotcha.zip</file>
</files>
</extension>
Манифест библиотеки — короткий: имя определяет папку, в которую она встанет
(libraries/gotcha):
<extension type="library" method="upgrade">
<name>LIB_GOTCHA</name>
<libraryname>gotcha</libraryname>
<version>1.2.1</version>
<files>
<folder>vendor</folder>
</files>
</extension>
Плагин подключает автолоадер из библиотеки, а не из своей папки:
// Зависимости живут в библиотеке пакета (libraries/gotcha), а не в
// папке плагина: так их можно обновлять отдельно и переиспользовать.
require_once JPATH_LIBRARIES . '/gotcha/vendor/autoload.php';
Дальше по тексту — устройство самого плагина.
Файлы плагина
plg_system_gotcha.zip
├── gotcha.xml # манифест
├── script.php # installer script: проверки + автовключение
├── services/provider.php # регистрация (Joomla 4/5/6)
├── src/Extension/Gotcha.php # вся логика
├── language/{ru-RU,en-GB}/ # строки интерфейса и сообщений
└── media/js/sentry.min.js # браузерный SDK (соберём ниже)
Манифест плагина gotcha.xml
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>PLG_SYSTEM_GOTCHA</name>
<author>Gotcha</author>
<authorUrl>https://getgotcha.ru</authorUrl>
<creationDate>2026-08</creationDate>
<license>MIT</license>
<version>1.2.1</version>
<description>PLG_SYSTEM_GOTCHA_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Gotcha</namespace>
<scriptfile>script.php</scriptfile>
<files>
<folder plugin="gotcha">services</folder>
<folder>src</folder>
<folder>language</folder>
</files>
<languages folder="language">
<language tag="en-GB">en-GB/plg_system_gotcha.ini</language>
<language tag="en-GB">en-GB/plg_system_gotcha.sys.ini</language>
<language tag="ru-RU">ru-RU/plg_system_gotcha.ini</language>
<language tag="ru-RU">ru-RU/plg_system_gotcha.sys.ini</language>
</languages>
<media destination="plg_system_gotcha" folder="media">
<folder>js</folder>
</media>
<config>
<fields name="params">
<fieldset name="basic">
<field name="dsn" type="text" size="60"
label="PLG_SYSTEM_GOTCHA_FIELD_DSN_LABEL"
description="PLG_SYSTEM_GOTCHA_FIELD_DSN_DESC"/>
<field name="environment" type="text" default="production"
label="PLG_SYSTEM_GOTCHA_FIELD_ENVIRONMENT_LABEL"
description="PLG_SYSTEM_GOTCHA_FIELD_ENVIRONMENT_DESC"/>
<field name="traces_sample_rate" type="text" default="0.2"
label="PLG_SYSTEM_GOTCHA_FIELD_TSR_LABEL"
description="PLG_SYSTEM_GOTCHA_FIELD_TSR_DESC"/>
<field name="browser" type="radio" default="1"
label="PLG_SYSTEM_GOTCHA_FIELD_BROWSER_LABEL"
description="PLG_SYSTEM_GOTCHA_FIELD_BROWSER_DESC"
layout="joomla.form.field.radio.switcher">
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
</fieldset>
</fields>
</config>
</extension>
Installer script script.php: проверки и сообщение после установки
Joomla умеет запускать скрипт установщика — им закрывают две задачи. Первая:
не дать поставить расширение туда, где оно не заработает (preflight с
проверкой версий Joomla и PHP). Вторая: показать человеку, что делать дальше.
Пустое окно «установлено успешно» — упущенный шанс: в postflight можно вывести
HTML со ссылками на документацию, установку сервера и статью.
public function preflight(string $type, InstallerAdapter $adapter): bool
{
if ($type === 'uninstall') {
return true;
}
if (!(new Version())->isCompatible($this->minimumJoomla)) {
$this->app->enqueueMessage(
Text::sprintf('PLG_SYSTEM_GOTCHA_ERROR_COMPATIBLE_JOOMLA', $this->minimumJoomla),
'error'
);
return false;
}
return true;
}
public function install(InstallerAdapter $adapter): bool
{
// Включаем плагин сразу после установки: без DSN он полный no-op,
// а пользователю остаётся только вписать DSN.
$plugin = new \stdClass();
$plugin->type = 'plugin';
$plugin->element = $adapter->getElement();
$plugin->folder = (string) $adapter->getParent()->manifest->attributes()['group'];
$plugin->enabled = 1;
$this->db->updateObject('#__extensions', $plugin, ['type', 'element', 'folder']);
return true;
}
public function postflight(string $type, InstallerAdapter $adapter): bool
{
if ($type === 'uninstall') {
return true;
}
$html = '<div class="row m-0">'
. '<div class="col-12 col-md-8 p-0 pe-3">'
. '<h2>' . Text::_('PLG_SYSTEM_GOTCHA_AFTER_' . strtoupper($type)) . '</h2>'
. Text::_('PLG_SYSTEM_GOTCHA_POSTINSTALL_BODY')
. '</div>'
. '<div class="col-12 col-md-4 p-0">'
. '<a class="btn btn-primary w-100" href="https://getgotcha.ru" target="_blank">getgotcha.ru</a>'
. '<a class="btn btn-outline-primary w-100" href="' . Text::_('PLG_SYSTEM_GOTCHA_LINK_DOCS_URL') . '" target="_blank">'
. Text::_('PLG_SYSTEM_GOTCHA_LINK_DOCS') . '</a>'
. '</div></div>';
$this->app->enqueueMessage($html, 'info');
return true;
}
Сообщение показывает только пакет. У плагина свой
script.phpсо своимpostflight(), и если вывести приветствие и там, и в пакете — при установке пользователь увидит один и тот же текст дважды. В плагине оставляем проверки версий и автовключение, приветствие — на уровне пакета.
Тексты — не в коде, а в языковых файлах language/ru-RU/plg_system_gotcha.sys.ini
и en-GB/. Суффикс .sys.ini важен: этот файл Joomla читает в менеджере
расширений и во время установки, а обычный .ini — когда пользователь открывает
настройки плагина. Оба нужны:
PLG_SYSTEM_GOTCHA="System - Gotcha"
PLG_SYSTEM_GOTCHA_AFTER_INSTALL="Спасибо за установку плагина Gotcha!"
PLG_SYSTEM_GOTCHA_POSTINSTALL_BODY="<p>Плагин отправляет в ваш инстанс Gotcha: <strong>ошибки PHP</strong>…</p>"
PLG_SYSTEM_GOTCHA_LINK_DOCS="Документация"
PLG_SYSTEM_GOTCHA_LINK_DOCS_URL="https://getgotcha.ru/docs/sdk/"
PLG_SYSTEM_GOTCHA_ERROR_COMPATIBLE_JOOMLA="Плагин Gotcha совместим с Joomla %s и выше."
В манифесте на них ссылаются ключами, а не текстом: <name>PLG_SYSTEM_GOTCHA</name>,
<description>PLG_SYSTEM_GOTCHA_XML_DESCRIPTION</description> и label/description
у каждого поля настроек. Тогда админка на русском покажет русские подписи, на
английском — английские.
Регистрация services/provider.php
Стандартный для Joomla 4/5 сервис-провайдер:
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Gotcha\Extension\Gotcha;
return new class () implements ServiceProviderInterface {
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$plugin = new Gotcha(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'gotcha')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};
Логика src/Extension/Gotcha.php
<?php
namespace Joomla\Plugin\System\Gotcha\Extension;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
use Sentry\SentrySdk;
use Sentry\Tracing\Transaction;
use Sentry\Tracing\TransactionContext;
\defined('_JEXEC') or die;
final class Gotcha extends CMSPlugin implements SubscriberInterface
{
private ?Transaction $transaction = null;
public static function getSubscribedEvents(): array
{
return [
'onAfterInitialise' => 'init',
'onAfterRoute' => 'startTransaction',
'onError' => 'captureError',
'onBeforeCompileHead' => 'injectBrowserSdk',
'onAfterRespond' => 'finishTransaction',
];
}
public function init(): void
{
$dsn = trim((string) $this->params->get('dsn', ''));
if ($dsn === '') {
return; // пустой DSN — плагин полностью выключен
}
require_once __DIR__ . '/../../vendor/autoload.php';
\Sentry\init([
'dsn' => $dsn,
'environment' => (string) $this->params->get('environment', 'production'),
'release' => 'joomla@' . JVERSION,
'traces_sample_rate' => (float) $this->params->get('traces_sample_rate', 0.2),
]);
}
public function startTransaction(): void
{
if (!class_exists(SentrySdk::class)) {
return; // init не отработал — DSN пуст
}
$input = $this->getApplication()->getInput();
// Имя — по компоненту и view, а не по URL: SEF-адресов миллионы,
// а "эндпойнтов" — десятки. Иначе раздел «Производительность»
// превратится в свалку уникальных транзакций.
$name = $input->getCmd('option', 'core') . '.' . $input->getCmd('view', 'default');
$this->transaction = \Sentry\startTransaction(
TransactionContext::make()->setName($name)->setOp('http.server')
);
SentrySdk::getCurrentHub()->setSpan($this->transaction);
}
public function captureError(Event $event): void
{
if (!class_exists(SentrySdk::class)) {
return;
}
// В Joomla 4/5 это ErrorEvent с getError(); generic-доступ через
// 'subject' работает и там, и в Joomla 6.
$error = method_exists($event, 'getError')
? $event->getError()
: $event->getArgument('subject');
if (!$error instanceof \Throwable) {
return;
}
// 401/403/404/405 — обычный веб-шум (сканеры, битые ссылки),
// а не ошибки приложения.
if (\in_array((int) $error->getCode(), [401, 403, 404, 405], true)) {
return;
}
\Sentry\captureException($error);
}
public function injectBrowserSdk(): void
{
$app = $this->getApplication();
$dsn = trim((string) $this->params->get('dsn', ''));
if ($dsn === '' || !(bool) $this->params->get('browser', 1) || !$app->isClient('site')) {
return;
}
$wa = $app->getDocument()->getWebAssetManager();
$wa->registerAndUseScript('plg_system_gotcha.sdk', 'plg_system_gotcha/sentry.min.js');
$wa->addInlineScript(
'Sentry.init({'
. 'dsn: ' . json_encode($dsn) . ','
. 'environment: ' . json_encode((string) $this->params->get('environment', 'production')) . ','
. 'integrations: [Sentry.browserTracingIntegration()],'
. 'tracesSampleRate: ' . (float) $this->params->get('traces_sample_rate', 0.2)
. '});',
[],
[],
['plg_system_gotcha.sdk']
);
}
public function finishTransaction(): void
{
if ($this->transaction === null) {
return;
}
$this->transaction->setHttpStatus(http_response_code());
$this->transaction->finish();
}
}
Обратите внимание на имя транзакции: com_content.article,
com_virtuemart.category — это и есть «эндпойнты» Joomla. Если назвать
транзакции по URL, каждый SEF-адрес станет отдельной строкой и раздел
«Производительность» потеряет смысл — подробнее в
Кардинальности.
Браузерный бандл
У классической Joomla нет фронтенд-сборки, поэтому соберём самодостаточный
бандл @sentry/browser один раз, любым бандлером — например esbuild:
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
browserTracingIntegration сам снимает Web Vitals (LCP, CLS, INP, FCP,
TTFB) и ловит JS-ошибки на странице. Если Gotcha живёт на другом домене, чем
сайт, — это нормальный случай: приёмник отвечает CORS-заголовками, браузер шлёт
напрямую, без прокси. Единственное, что может помешать, — ваш собственный
Content-Security-Policy: добавьте адрес инстанса в connect-src.
Обязательный шаг: изоляция vendor
Вот та грабля, на которую наступают почти все, кто первый раз пакует
composer-зависимости в расширение CMS. Пакет несёт свой vendor/, но и Joomla
несёт свой — и там встречаются те же пакеты. Классы называются одинаково, а
версии разные. Кто первым зарегистрировал автолоадер, тот и определил, чей
класс загрузится, а дальше — 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 5 и 6 всё работало, а Joomla 4.4 слегла в белый экран на каждой странице. И заметьте: сломался бы сайт целиком, а не мониторинг.
Напрашивается решение «исключить эти пакеты из composer и пользоваться теми,
что уже есть в Joomla». Не выйдет — посмотрите, что реально лежит в
libraries/vendor разных версий:
| Пакет | Joomla 4.4 | Joomla 5 | Joomla 6 |
|---|---|---|---|
psr/log | 1.1.4 | 3.0.2 | 3.0.2 |
psr/http-message | 1.1 | 1.1 | 2.0 |
psr/container | 1.1.1 | 1.1.2 | 2.0.2 |
symfony/options-resolver | v5.4 | v6.4 | v7.4 |
symfony/deprecation-contracts | v2.5 | v3.6 | v3.6 |
guzzlehttp/psr7 | нет | 2.12.3 | 2.12.3 |
Мажорные версии разъезжаются, а guzzlehttp/psr7 в Joomla 4 нет вовсе. Опора
на библиотеки ядра означала бы отдельную сборку под каждую версию Joomla —
и переставать работать при каждом крупном релизе.
Лечится это переименованием namespace зависимостей при сборке —
php-scoper. Он проходит по vendor/ и
нашему коду и приписывает всем чужим классам префикс, после чего наши копии
перестают существовать для всех остальных:
// scoper.inc.php
return [
'prefix' => 'Gotcha\\Vendor',
'finders' => [
Finder::create()->files()->in('vendor'),
Finder::create()->files()->name('*.php')->in('plugin/src'),
],
// Namespace самого плагина не трогаем — по нему Joomla его находит.
// Ссылки на Sentry внутри наших файлов php-scoper перепишет сам.
'exclude-namespaces' => ['Joomla'],
];
После сборки в коде плагина \Sentry\init(...) превращается в
\Gotcha\Vendor\Sentry\init(...), а vendor/psr/log живёт в
Gotcha\Vendor\Psr\Log. Конфликт исчезает навсегда — и с ядром, и с любым
другим расширением, которое притащило свой Sentry или Guzzle. Один архив
работает на 4.2+, 5 и 6.
Важная деталь сборки: скоупить нужно за один прогон и vendor/, и код
плагина. Разложить их по двум архивам (библиотека и плагин) можно только
потом — иначе ссылки на Sentry в плагине не совпадут с префиксом в библиотеке.
Правило простое: любое расширение CMS с собственным vendor/ нужно
скоупить. Это верно и для Joomla, и для WordPress, и для Битрикса — везде,
где расширения живут в одном PHP-процессе.
Установка
Собираем zip и ставим штатно:
./build.sh # composer install -> php-scoper -> два zip -> пакет
Либо берём готовый пакет — это ровно тот
код, что в статье, вместе с изолированным vendor/ и собранным браузерным
бандлом (проверен на Joomla 4.4, 5.4 и 6 из официальных docker-образов).
Система → Установка → Расширения, загрузить pkg_gotcha-1.2.1.zip — Joomla
поставит и библиотеку, и плагин, плагин включится сам. Дальше Система →
Плагины → System - Gotcha, вписать DSN. Проверка — временно бросить исключение в любом
шаблоне или обратиться к несуществующему компоненту с кодом 500; событие
появится в разделе «Проблемы» через несколько секунд.
Какие метрики собирать с сайта
Самое ценное в мониторинге сайта на CMS — не экзотика, а несколько скучных показателей, которые закрывают 90% вопросов «а что с сайтом?»:
| Метрика | Зачем | Откуда берётся |
|---|---|---|
| Ошибки PHP по релизам | «после обновления расширения посыпались 500-е» | плагин, onError |
| Ошибки JS в браузере | слайдер/форма сломаны только у части посетителей | @sentry/browser |
| p95 времени ответа по компонентам | какой компонент тормозит: контент, магазин, поиск | транзакции |
| Web Vitals: LCP, INP, CLS | реальная скорость у посетителей, а не в Lighthouse | browserTracingIntegration |
| Аптайм и срок SSL-сертификата | сайт лежит / сертификат истекает через 3 дня | HTTP-монитор Gotcha |
| Бизнес-метрики: заказы, регистрации | «мониторинг зелёный, а заказов нет» — самый коварный отказ | OTLP, cron |
Первые четыре строки уже закрыты плагином выше. Остались две.
Аптайм: без единой строки кода
Gotcha сам ходит на публичный URL снаружи. В проекте: Аптайм → Новый монитор → HTTP, URL сайта, интервал и пороги. Инциденты, SSL-алерты и публичная status-страница — из коробки. Подробнее — Аптайм.
Бизнес-метрики по OTLP
Числовые метрики Gotcha принимает по OTLP/HTTP — POST на /v1/metrics с
заголовком Authorization: Bearer <public_key> (ключ — часть DSN между
https:// и @). Для Joomla-сайта проще всего снять цифры прямо из базы
кроном. Например, число пользователей раз в 5 минут:
#!/bin/bash
USERS=$(mysql -N -e "SELECT COUNT(*) FROM j_users" joomla_db)
curl -s -X POST https://gotcha.example.com/v1/metrics \
-H "Authorization: Bearer <public_key>" \
-H "Content-Type: application/json" \
-d '{"resourceMetrics":[{"resource":{"attributes":[
{"key":"service.name","value":{"stringValue":"joomla-site"}}]},
"scopeMetrics":[{"metrics":[{"name":"users_total","gauge":{"dataPoints":[
{"asDouble":'"$USERS"',"timeUnixNano":"'"$(date +%s%N)"'"}]}}]}]}]}'
Тем же способом отправляются заказы VirtueMart/HikaShop, отправки форм, размер
очереди писем — любое число из SELECT COUNT(*). На метрики вешаются пороговые
алерты («заказов за час = 0 в рабочее время» — сигнал не хуже пятисоток).
Подробнее — Метрики и Алерты по метрикам.
Алерты
В разделе «Оповещения» правила (новый issue, регрессия, всплеск) уже включены; остаётся добавить канал доставки — Telegram, webhook или email. Каналы привязываются и к аптайм-мониторам, и к пороговым правилам по метрикам. Подробнее — Алерты.
Итог
- Официального SDK для Joomla нет — и не нужно: один системный плагин на
~100 строк подключает
sentry/sentryи закрывает ошибки, трейсинг и Web Vitals. Одинаково работает на Joomla 4.2+, 5 и 6. onErrorвместоset_exception_handler: Joomla перехватывает исключения сама, забирать их нужно из события.- Транзакции — вручную,
onAfterRoute→onAfterRespond, имя поoption.view, а не по URL. - Пакет, а не голый плагин:
pkg_из библиотеки с зависимостями и системного плагина — так принято в Joomla и так удобнее обновлять. vendor/обязательно скоупить php-scoper’ом: версииpsr/log,psr/http-messageиsymfony/*в Joomla 4, 5 и 6 несовместимы между собой, и без префикса конфликт роняет весь сайт, а не только мониторинг.- Аптайм и SSL — монитор на публичный URL, кода не требует.
- Бизнес-метрики — cron + OTLP POST, пороговые алерты в UI.
Дальше — документация, установка и раздел подключения SDK.