feat(pulse): make heartbeat opt-in with transparency toggle

Heartbeat telemetry (домен, имя бота, версии PHP/модуля/OpenCart) больше не
отправляется скрытно. Добавлен отдельный флаг pulse.heartbeat_enabled
(по умолчанию выключен, opt-in), независимый от pulse.api_key:

- ранний return в handleHeartbeat() при выключенном флаге (не трогает кэш/getMe);
- провайдер читает флаг и инжектит в сервис;
- в админке — переключатель с честным раскрытием состава данных;
- debug-лог факта отправки в pushHeartbeat().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 15:08:04 +03:00
parent 24075dceb7
commit 0198c8fcf2
5 changed files with 27 additions and 1 deletions

View File

@@ -88,6 +88,7 @@ export const useSettingsStore = defineStore('settings', {
pulse: { pulse: {
api_key: '', api_key: '',
heartbeat_enabled: false,
batch_size: 50, batch_size: 50,
}, },

View File

@@ -96,6 +96,16 @@
При малом значении нагрузка ниже, но обработка занимает больше времени. При малом значении нагрузка ниже, но обработка занимает больше времени.
Рекомендуемое значение: 50. Рекомендуемое значение: 50.
</ItemInput> </ItemInput>
<ItemBool label="Отправка технических данных разработчику"
v-model="settings.items.pulse.heartbeat_enabled"
>
Чтобы улучшать продукт и быстрее помогать в поддержке, модуль отправляет разработчику
минимальный набор технических данных: домен магазина, имя бота и версии PHP, модуля и
OpenCart. <strong>Персональные данные покупателей и информация о заказах не
передаются.</strong> По умолчанию отправка отключена; включить или отключить её можно
в любой момент на работу магазина это не влияет.
</ItemBool>
</div> </div>
</template> </template>
@@ -103,6 +113,7 @@
import {onMounted, ref} from "vue"; import {onMounted, ref} from "vue";
import {useSettingsStore} from "@/stores/settings.js"; import {useSettingsStore} from "@/stores/settings.js";
import ItemInput from "@/components/Settings/ItemInput.vue"; import ItemInput from "@/components/Settings/ItemInput.vue";
import ItemBool from "@/components/Settings/ItemBool.vue";
import {apiGet} from "@/utils/http.js"; import {apiGet} from "@/utils/http.js";
import SettingsItem from "@/components/SettingsItem.vue"; import SettingsItem from "@/components/SettingsItem.vue";

View File

@@ -85,6 +85,7 @@ HTML,
'pulse' => [ 'pulse' => [
'api_key' => '', 'api_key' => '',
'heartbeat_enabled' => false,
'batch_size' => 50, 'batch_size' => 50,
'max_attempts' => 3, 'max_attempts' => 3,
], ],

View File

@@ -22,6 +22,7 @@ class TeleCartPulseService
private TeleCartEvent $eventModel; private TeleCartEvent $eventModel;
private ?string $apiKey; private ?string $apiKey;
private ?PayloadSigner $heartbeatPayloadSigner; private ?PayloadSigner $heartbeatPayloadSigner;
private bool $heartbeatEnabled;
public function __construct( public function __construct(
TelegramInitDataDecoder $initDataDecoder, TelegramInitDataDecoder $initDataDecoder,
@@ -30,7 +31,8 @@ class TeleCartPulseService
LoggerInterface $logger, LoggerInterface $logger,
TeleCartEvent $eventModel, TeleCartEvent $eventModel,
?string $apiKey = null, ?string $apiKey = null,
?PayloadSigner $heartbeatPayloadSigner = null ?PayloadSigner $heartbeatPayloadSigner = null,
bool $heartbeatEnabled = false
) { ) {
$this->initDataDecoder = $initDataDecoder; $this->initDataDecoder = $initDataDecoder;
$this->telegramService = $telegramService; $this->telegramService = $telegramService;
@@ -39,6 +41,7 @@ class TeleCartPulseService
$this->eventModel = $eventModel; $this->eventModel = $eventModel;
$this->apiKey = $apiKey; $this->apiKey = $apiKey;
$this->heartbeatPayloadSigner = $heartbeatPayloadSigner; $this->heartbeatPayloadSigner = $heartbeatPayloadSigner;
$this->heartbeatEnabled = $heartbeatEnabled;
} }
/** /**
@@ -132,6 +135,10 @@ class TeleCartPulseService
*/ */
public function handleHeartbeat(): void public function handleHeartbeat(): void
{ {
if (! $this->heartbeatEnabled) {
return;
}
if ($this->cache->get('telecart_pulse_heartbeat')) { if ($this->cache->get('telecart_pulse_heartbeat')) {
return; return;
} }
@@ -193,6 +200,10 @@ class TeleCartPulseService
*/ */
private function pushHeartbeat(array $json): void private function pushHeartbeat(array $json): void
{ {
$this->logger->debug('Pushing heartbeat', [
'data' => $json,
]);
$baseUri = rtrim(env('PULSE_API_HOST', 'http://localhost'), '/') . '/'; $baseUri = rtrim(env('PULSE_API_HOST', 'http://localhost'), '/') . '/';
$client = new Client([ $client = new Client([

View File

@@ -22,6 +22,7 @@ class TeleCartPulseServiceProvider extends ServiceProvider
$this->container->singleton(TeleCartPulseService::class, function (Container $app) { $this->container->singleton(TeleCartPulseService::class, function (Container $app) {
$heartbeatSecret = $app->getConfigValue('pulse.heartbeat_secret') ?? env('PULSE_HEARTBEAT_SECRET'); $heartbeatSecret = $app->getConfigValue('pulse.heartbeat_secret') ?? env('PULSE_HEARTBEAT_SECRET');
$heartbeatSigner = $heartbeatSecret ? new PayloadSigner($heartbeatSecret) : null; $heartbeatSigner = $heartbeatSecret ? new PayloadSigner($heartbeatSecret) : null;
$heartbeatEnabled = (bool) $app->getConfigValue('pulse.heartbeat_enabled');
return new TeleCartPulseService( return new TeleCartPulseService(
$app->get(TelegramInitDataDecoder::class), $app->get(TelegramInitDataDecoder::class),
@@ -31,6 +32,7 @@ class TeleCartPulseServiceProvider extends ServiceProvider
$app->get(TeleCartEvent::class), $app->get(TeleCartEvent::class),
$app->getConfigValue('pulse.api_key'), $app->getConfigValue('pulse.api_key'),
$heartbeatSigner, $heartbeatSigner,
$heartbeatEnabled,
); );
}); });