feat(pulse): implement reliable event tracking and delivery system

Implement comprehensive event tracking system for TeleCart Pulse that ensures
all user interactions and order events are reliably captured and delivered to
the analytics platform, even in case of network failures or service outages.

Business Value:
- Guaranteed event delivery: All events are stored in database before sending,
  ensuring no data loss even if SaaS service is temporarily unavailable
- Automatic retry mechanism: Failed events are automatically retried with
  configurable attempts, reducing manual intervention
- Real-time monitoring: Admin dashboard displays event statistics (pending,
  sent, failed) to track system health and delivery status
- Data integrity: Idempotency keys prevent duplicate events, ensuring accurate
  analytics and metrics
- Performance optimization: Statistics are cached for 1 hour to reduce database
  load while maintaining visibility

Key Features:
- Event queue system: Events are queued in database with status tracking
  (pending/sent/failed)
- Asynchronous processing: Events are sent via background tasks, not blocking
  user interactions
- Error tracking: Failed events include detailed error reasons for debugging
- Campaign tracking: Only events with valid campaign_id and tracking_id are
  stored, ensuring data quality
- Admin visibility: Statistics dashboard shows delivery status at a glance

This system ensures reliable data collection for campaign analytics, A/B testing,
and performance metrics, providing accurate insights for business decisions.
This commit is contained in:
2025-12-07 19:46:33 +03:00
committed by Nikita Kiselev
parent 1f5ef4353d
commit 4a3dcc11d1
19 changed files with 745 additions and 5468 deletions

View File

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

View File

@@ -1,15 +1,97 @@
<template>
<ItemInput label="API ключ"
v-model="settings.items.pulse.api_key"
placeholder="AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"
>
Используется для обмена информацией по кампаниям, рассылкам, сбору метрик.
</ItemInput>
<div class="tw:space-y-6">
<SettingsItem v-if="settings.items.pulse.api_key" label="Статистика за 7 дней">
<template #default>
<div v-if="stats" class="tw:space-y-4">
<div class="tw:flex tw:gap-3 tw:max-w-2xl">
<div class="tw:group tw:bg-white tw:rounded-lg tw:shadow tw:p-3 tw:relative tw:flex-1 tw:transition-all tw:duration-200 tw:cursor-default tw:hover:shadow-md tw:hover:-translate-y-0.5">
<div class="tw:flex tw:justify-between tw:items-start tw:mb-1.5">
<div class="tw:text-xs tw:font-medium tw:text-gray-700">В очереди</div>
<div
class="tw:w-8 tw:h-8 tw:rounded-lg tw:bg-gradient-to-br tw:from-yellow-400 tw:to-yellow-600 tw:flex tw:items-center tw:justify-center tw:transition-transform tw:duration-200 tw:group-hover:scale-110">
<i class="fa fa-clock-o tw:text-white tw:text-xs"></i>
</div>
</div>
<div class="tw:text-3xl tw:font-bold tw:text-gray-800 tw:mb-0.5">{{
stats.pending
}}
</div>
<div class="tw:text-xs tw:text-gray-500">Ожидают отправки</div>
</div>
<div class="tw:group tw:bg-white tw:rounded-lg tw:shadow tw:p-3 tw:relative tw:flex-1 tw:transition-all tw:duration-200 tw:cursor-default tw:hover:shadow-md tw:hover:-translate-y-0.5">
<div class="tw:flex tw:justify-between tw:items-start tw:mb-1.5">
<div class="tw:text-xs tw:font-medium tw:text-gray-700">Отправлено</div>
<div
class="tw:w-8 tw:h-8 tw:rounded-lg tw:bg-gradient-to-br tw:from-green-400 tw:to-green-600 tw:flex tw:items-center tw:justify-center tw:transition-transform tw:duration-200 tw:group-hover:scale-110">
<i class="fa fa-check-circle tw:text-white tw:text-xs"></i>
</div>
</div>
<div class="tw:text-3xl tw:font-bold tw:text-gray-800 tw:mb-0.5">{{
stats.sent
}}
</div>
<div class="tw:text-xs tw:text-gray-500">Успешно доставлено</div>
</div>
<div class="tw:group tw:bg-white tw:rounded-lg tw:shadow tw:p-3 tw:relative tw:flex-1 tw:transition-all tw:duration-200 tw:cursor-default tw:hover:shadow-md tw:hover:-translate-y-0.5">
<div class="tw:flex tw:justify-between tw:items-start tw:mb-1.5">
<div class="tw:text-xs tw:font-medium tw:text-gray-700">Ошибки</div>
<div
class="tw:w-8 tw:h-8 tw:rounded-lg tw:bg-gradient-to-br tw:from-red-400 tw:to-red-600 tw:flex tw:items-center tw:justify-center tw:transition-transform tw:duration-200 tw:group-hover:scale-110">
<i class="fa fa-exclamation-circle tw:text-white tw:text-xs"></i>
</div>
</div>
<div class="tw:text-3xl tw:font-bold tw:text-gray-800 tw:mb-0.5">{{
stats.failed
}}
</div>
<div class="tw:text-xs tw:text-gray-500">Требуют внимания</div>
</div>
</div>
</div>
</template>
<template #help>
Статистика обновляется 1 раз в час
</template>
</SettingsItem>
<ItemInput label="API ключ"
v-model="settings.items.pulse.api_key"
placeholder="AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"
>
Используется для обмена информацией по кампаниям, рассылкам, сбору метрик.
</ItemInput>
<ItemInput label="Размер пакета обработки"
v-model.number="settings.items.pulse.batch_size"
type="number"
placeholder="50"
>
Определяет, сколько событий отправляется в TeleCart Pulse за один запуск фоновой задачи.
При большом значении события обрабатываются быстрее, но увеличивается нагрузка на сервер.
При малом значении нагрузка ниже, но обработка занимает больше времени.
Рекомендуемое значение: 50.
</ItemInput>
</div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import {useSettingsStore} from "@/stores/settings.js";
import ItemInput from "@/components/Settings/ItemInput.vue";
import {apiGet} from "@/utils/http.js";
import SettingsItem from "@/components/SettingsItem.vue";
const settings = useSettingsStore();
const stats = ref(null);
const loadStats = async () => {
const response = await apiGet('getTeleCartPulseStats');
if (response.success) {
stats.value = response.data;
}
};
onMounted(() => {
loadStats();
});
</script>

View File

@@ -20,14 +20,16 @@ export const usePulseStore = defineStore('pulse', {
},
ingest(event, eventData = {}) {
const idempotencyKey = crypto.randomUUID();
ingest({
event: event,
idempotency_key: idempotencyKey,
payload: {
webapp: window.Telegram.WebApp,
eventData: eventData,
},
})
.then(() => console.debug('[Pulse] Event Ingested', event, eventData))
.then(() => console.debug('[Pulse] Event Ingested', event, eventData, idempotencyKey))
.catch(err => console.error('Ingest failed:', err));
},