docs: add design spec for honest opt-out heartbeat
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
|||||||
|
# Дизайн: честный и отключаемый heartbeat
|
||||||
|
|
||||||
|
Дата: 2026-06-30
|
||||||
|
Статус: согласовано (ожидает финального ревью)
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
Модуль скрытно отправляет на сервер разработчика (`pulse.telecart.pro`) сведения об
|
||||||
|
окружении, где он запущен (heartbeat). Покупатель об этом не уведомляется и не может
|
||||||
|
это отключить. Это нечестно по отношению к покупателю и создаёт риски (GDPR / 152-ФЗ,
|
||||||
|
претензии маркетплейсов).
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Сделать heartbeat **честным**: прозрачным и отключаемым, сохранив поведение по умолчанию.
|
||||||
|
|
||||||
|
- **Прозрачность:** в админ-панели покупатель видит, что именно собирается, куда
|
||||||
|
отправляется и зачем.
|
||||||
|
- **Контроль:** отдельный переключатель, которым покупатель может выключить heartbeat.
|
||||||
|
- **По умолчанию включено** (текущее поведение сохраняется, но становится видимым и
|
||||||
|
управляемым).
|
||||||
|
|
||||||
|
## Границы (scope)
|
||||||
|
|
||||||
|
- **Только heartbeat.** Остальной TeleCart Pulse (Telegram-рассылки, события кампаний)
|
||||||
|
**не трогаем** — он гейтится наличием `pulse.api_key` и, по позиции владельца, прав не
|
||||||
|
нарушает.
|
||||||
|
- Концептуально heartbeat считаем частью фичи **TeleCart Pulse** (одна папка доков), но
|
||||||
|
у него **отдельный собственный выключатель**, не завязанный на `pulse.api_key` и не
|
||||||
|
завязанный на секрет подписи. Pulse и heartbeat включаются/выключаются независимо.
|
||||||
|
|
||||||
|
## Терминология
|
||||||
|
|
||||||
|
- **heartbeat** — периодическая (раз в час по кэшу) отправка информации об окружении:
|
||||||
|
`domain`, `bot_name`, `php_version`, `module_version`, `opencart_version`,
|
||||||
|
`opencart_version_core`. Триггерится из SPA при открытии WebApp.
|
||||||
|
- **TeleCart Pulse** — фича рассылок и аналитики кампаний; heartbeat живёт внутри её
|
||||||
|
namespace в коде (`framework/TeleCartPulse/`).
|
||||||
|
|
||||||
|
## Текущее устройство (как есть)
|
||||||
|
|
||||||
|
- `TeleCartPulseService::handleHeartbeat()`
|
||||||
|
(`module/.../framework/TeleCartPulse/TeleCartPulseService.php:133`) — ставит кэш-флаг
|
||||||
|
`telecart_pulse_heartbeat` (ttl 3600), дёргает `getMe()`, формирует payload и шлёт POST
|
||||||
|
на `PULSE_API_HOST/heartbeat`.
|
||||||
|
- Отправка фактически происходит только если есть `heartbeatPayloadSigner` (строки 168–170),
|
||||||
|
который создаётся в провайдере при наличии секрета (`pulse.heartbeat_secret` /
|
||||||
|
`PULSE_HEARTBEAT_SECRET`).
|
||||||
|
- `TeleCartPulseServiceProvider`
|
||||||
|
(`module/.../framework/TeleCartPulse/TeleCartPulseServiceProvider.php:22-35`) читает
|
||||||
|
значения из конфига и инжектит их в `TeleCartPulseService` через конструктор.
|
||||||
|
- **Пользовательской (per-install) настройки для heartbeat сейчас нет.**
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
### Принцип
|
||||||
|
|
||||||
|
Использовать существующий паттерн: провайдер читает значение из конфига и **инжектит**
|
||||||
|
его в сервис через конструктор (как уже делается для `pulse.api_key` и signer). Не вводить
|
||||||
|
новых механизмов гейтинга.
|
||||||
|
|
||||||
|
### 1. Backend
|
||||||
|
|
||||||
|
**`module/.../configs/app.php`** — секция `pulse` (строки 86–90): добавить флаг с дефолтом
|
||||||
|
`true`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
'pulse' => [
|
||||||
|
'api_key' => '',
|
||||||
|
'heartbeat_enabled' => true, // новый флаг
|
||||||
|
'batch_size' => 50,
|
||||||
|
'max_attempts' => 3,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
**`TeleCartPulseServiceProvider`** — прочитать `pulse.heartbeat_enabled` и передать булевый
|
||||||
|
флаг в конструктор `TeleCartPulseService` (рядом с `apiKey` и `heartbeatSigner`). Привести
|
||||||
|
к `bool` явно.
|
||||||
|
|
||||||
|
**`TeleCartPulseService`** — принять флаг в конструкторе (новое поле `private bool
|
||||||
|
$heartbeatEnabled`). В `handleHeartbeat()` — **ранний `return` в самом начале метода**, до
|
||||||
|
проверки/установки кэша и до `getMe()`, если флаг выключен:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function handleHeartbeat(): void
|
||||||
|
{
|
||||||
|
if (! $this->heartbeatEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ... существующая логика
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Совместимость с PHP 7.4: без named arguments / промоушена свойств / union-типов. Параметр
|
||||||
|
конструктора — позиционный, со значением по умолчанию `true` для обратной совместимости
|
||||||
|
вызовов в тестах.
|
||||||
|
|
||||||
|
### 2. Frontend (админка) + прозрачность
|
||||||
|
|
||||||
|
**`frontend/admin/src/stores/settings.js`** — секция `pulse` (строки 89–92): добавить
|
||||||
|
`heartbeat_enabled` в дефолтное состояние state.
|
||||||
|
|
||||||
|
**`frontend/admin/src/views/TeleCartPulseView.vue`** — добавить `ItemBool` с переключателем,
|
||||||
|
привязанным к `settings.items.pulse.heartbeat_enabled`. В слот-описание — **честное, но
|
||||||
|
нейтральное по тону раскрытие**:
|
||||||
|
|
||||||
|
- перечислить, что отправляется: домен, имя бота, версия PHP, версия модуля, версии
|
||||||
|
OpenCart;
|
||||||
|
- указать адрес назначения: `pulse.telecart.pro`;
|
||||||
|
- описать цель мягко: для улучшения продукта и техподдержки (без акцента на проверке
|
||||||
|
лицензии).
|
||||||
|
|
||||||
|
Точную формулировку владелец отредактирует; черновик в нейтральном тоне готовит реализация.
|
||||||
|
|
||||||
|
### 3. Поведение для существующих установок
|
||||||
|
|
||||||
|
Дефолт `true` в конфиге мержится с сохранёнными настройками
|
||||||
|
(`ApplicationFactory`: `Arr::mergeArraysRecursively`). У установок без сохранённого значения
|
||||||
|
heartbeat остаётся включённым — поведение не меняется, но теперь переключатель виден.
|
||||||
|
Когда покупатель выключает тумблер и сохраняет, значение пишется в `oc_setting`
|
||||||
|
(`module_telecart_settings`) и читается на backend.
|
||||||
|
|
||||||
|
### 4. Гейтинг — итоговая логика
|
||||||
|
|
||||||
|
heartbeat реально отправляется только если **одновременно**: `heartbeat_enabled = true`
|
||||||
|
**и** есть signer (секрет настроен). Переключатель — пользовательский контроль поверх
|
||||||
|
существующего серверного гейта; они независимы.
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
Unit-тест на `TeleCartPulseService::handleHeartbeat()`:
|
||||||
|
|
||||||
|
- `heartbeat_enabled = false` → метод выходит сразу: HTTP-запрос не выполняется, кэш-флаг
|
||||||
|
`telecart_pulse_heartbeat` **не** ставится, `getMe()` не вызывается.
|
||||||
|
- `heartbeat_enabled = true` + signer присутствует → payload формируется и уходит (как
|
||||||
|
сейчас).
|
||||||
|
|
||||||
|
Запуск: `make test-unit` (или `./vendor/bin/phpunit --filter ...` через `make ssh`).
|
||||||
|
|
||||||
|
## Документация (в этом порядке)
|
||||||
|
|
||||||
|
1. **`docs/features/settings/CLAUDE.md`** — каркас системы настроек: путь поля «форма в
|
||||||
|
админке → `saveSettingsForm` → таблица `oc_setting` (`module_telecart_settings`) →
|
||||||
|
`Config\Settings` / DTO → backend», и **чек-лист «как добавить новый toggle»** по слоям
|
||||||
|
(config-дефолт → провайдер/DTO → store → view-компонент `ItemBool`). По шаблону
|
||||||
|
`docs/features/_TEMPLATE.md`. Добавить строку в реестр в `docs/features/CLAUDE.md`.
|
||||||
|
2. **`docs/features/telecart-pulse/CLAUDE.md`** — фича Pulse целиком: рассылки, события
|
||||||
|
кампаний (гейт `api_key`) и heartbeat с новым выключателем. Ссылка на `[[settings]]`.
|
||||||
|
Добавить строку в реестр.
|
||||||
|
|
||||||
|
## Затрагиваемые файлы (сводка)
|
||||||
|
|
||||||
|
- `module/.../configs/app.php` — флаг `pulse.heartbeat_enabled`.
|
||||||
|
- `module/.../framework/TeleCartPulse/TeleCartPulseServiceProvider.php` — чтение и инжект.
|
||||||
|
- `module/.../framework/TeleCartPulse/TeleCartPulseService.php` — поле + ранний return.
|
||||||
|
- `frontend/admin/src/stores/settings.js` — дефолт в state.
|
||||||
|
- `frontend/admin/src/views/TeleCartPulseView.vue` — `ItemBool` + текст раскрытия.
|
||||||
|
- `module/.../tests/Unit/...` — тест на гейтинг heartbeat.
|
||||||
|
- `docs/features/settings/CLAUDE.md`, `docs/features/telecart-pulse/CLAUDE.md`,
|
||||||
|
`docs/features/CLAUDE.md` (реестр).
|
||||||
|
|
||||||
|
## Явно вне scope (YAGNI)
|
||||||
|
|
||||||
|
- Не делаем opt-in (по умолчанию остаётся включено).
|
||||||
|
- Не делаем отдельный consent-баннер на первой установке (достаточно видимого
|
||||||
|
переключателя с раскрытием).
|
||||||
|
- Не трогаем события кампаний и рассылки Pulse.
|
||||||
|
- Не выносим heartbeat в отдельный namespace/фичу из `TeleCartPulse`.
|
||||||
Reference in New Issue
Block a user