feat: track and push TeleCart Pulse events

This commit is contained in:
2025-11-30 16:52:32 +03:00
parent fc8044484e
commit ef785654b9
19 changed files with 583 additions and 70 deletions

View File

@@ -109,9 +109,11 @@
</template>
<template #body="{ data }">
<template v-if="col.field === 'id'">{{ data.id }}</template>
<template v-else-if="col.field === 'telegram_user_id'">{{
data.telegram_user_id
}}
<template v-else-if="col.field === 'telegram_user_id'">
{{ data.telegram_user_id }}
</template>
<template v-else-if="col.field === 'tracking_id'">
<code>{{ data.tracking_id }}</code>
</template>
<template v-else-if="col.field === 'username'">
<div class="tw:flex tw:items-center tw:gap-2">
@@ -180,7 +182,8 @@
<InputText v-model="filterModel.value" type="text" placeholder="Поиск по фамилии"
class="p-column-filter"/>
</template>
<template v-else-if="['last_seen_at', 'created_at', 'privacy_consented_at'].includes(col.field)">
<template
v-else-if="['last_seen_at', 'created_at', 'privacy_consented_at'].includes(col.field)">
<DatePicker v-model="filterModel.value" dateFormat="dd.mm.yy" placeholder="dd.mm.yyyy"/>
</template>
<template v-else-if="col.field === 'orders_count'">
@@ -314,6 +317,14 @@ const columns = ref([
filterable: true,
visible: false
},
{
field: 'tracking_id',
header: 'Tracking ID',
sortable: false,
filterable: true,
visible: false,
help: 'Tracking ID это публичный уникальный идентификатор покупателя, используется в рекламных кампаниях для отслеживания активности.',
},
{field: 'username', header: 'Имя пользователя', sortable: true, filterable: true, visible: true},
{field: 'first_name', header: 'Имя', sortable: true, filterable: true, visible: true},
{field: 'last_name', header: 'Фамилия', sortable: true, filterable: true, visible: true},
@@ -597,10 +608,10 @@ function resetFilters() {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
},
privacy_consented_at: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
},
privacy_consented_at: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
},
};
lazyParams.value.page = 1;
lazyParams.value.first = 0;

View File

@@ -1,3 +1,4 @@
export const TC_PULSE_EVENTS = {
WEBAPP_OPEN: 'WEBAPP_OPEN',
ORDER_CREATED: 'ORDER_CREATED',
};

View File

@@ -87,3 +87,39 @@ export function getCssVarOklchRgb(cssVarName) {
return `#${toHex(r)}${toHex(g)}${toHex(b_)}`;
}
export function deserializeStartParams(serialized) {
if (!serialized) {
return {};
}
// Восстанавливаем стандартные base64 символы
let encoded = serialized.replace(/-/g, '+').replace(/_/g, '/');
// Добавляем padding, если нужно
const padding = encoded.length % 4;
if (padding !== 0) {
encoded += '='.repeat(4 - padding);
}
// Декодируем из base64
let json;
try {
json = atob(encoded); // btoa / atob стандартные в браузере
} catch (e) {
throw new Error('Failed to decode base64 string');
}
// Парсим JSON
let parameters;
try {
parameters = JSON.parse(json);
} catch (e) {
throw new Error('Failed to decode JSON: ' + e.message);
}
if (typeof parameters !== 'object' || parameters === null || Array.isArray(parameters) && !Array.isArray(parameters)) {
throw new Error('Decoded value is not an object');
}
return parameters;
}

View File

@@ -8,7 +8,7 @@ import {useSettingsStore} from "@/stores/SettingsStore.js";
import ApplicationError from "@/ApplicationError.vue";
import AppMetaInitializer from "@/utils/AppMetaInitializer.ts";
import {injectYaMetrika} from "@/utils/yaMetrika.js";
import {checkIsUserPrivacyConsented, ingest, saveTelegramCustomer} from "@/utils/ftch.js";
import {checkIsUserPrivacyConsented} from "@/utils/ftch.js";
import {register} from 'swiper/element/bundle';
import 'swiper/element/bundle';
@@ -20,6 +20,7 @@ import {getCssVarOklchRgb} from "@/helpers.js";
import {defaultConfig, plugin} from '@formkit/vue';
import config from './formkit.config.js';
import {TC_PULSE_EVENTS} from "@/constants/tPulseEvents.js";
import {usePulseStore} from "@/stores/Pulse.js";
register();
@@ -34,6 +35,7 @@ app
const settings = useSettingsStore();
const blocks = useBlocksStore();
const pulse = usePulseStore();
const appLoading = createApp(AppLoading);
appLoading.mount('#app');
@@ -51,24 +53,9 @@ settings.load()
throw new Error('App disabled (maintenance mode)');
}
})
.then(() => {
const webapp = window.Telegram.WebApp;
ingest({
event: TC_PULSE_EVENTS.WEBAPP_OPEN,
webapp,
})
.catch(err => console.error('Ingest failed:', err));
})
.then(() => {
// Сохраняем данные Telegram-пользователя в базу данных
const userData = window.Telegram?.WebApp?.initDataUnsafe?.user;
if (userData) {
console.debug('[Init] Saving Telegram customer data');
saveTelegramCustomer(userData)
.then(() => console.debug('[Init] Telegram customer data saved successfully'))
.catch(() => console.warn('[Init] Failed to save Telegram customer data:', error));
}
})
.then(() => pulse.initFromStartParams())
.then(() => pulse.catchTelegramCustomerFromInitData())
.then(() => pulse.ingest(TC_PULSE_EVENTS.WEBAPP_OPEN))
.then(() => {
(async () => {
try {

View File

@@ -5,6 +5,9 @@ import {useCartStore} from "@/stores/CartStore.js";
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {useSettingsStore} from "@/stores/SettingsStore.js";
import {usePulseStore} from "@/stores/Pulse.js";
import {TC_PULSE_EVENTS} from "@/constants/tPulseEvents.js";
import {nextTick} from "vue";
export const useCheckoutStore = defineStore('checkout', {
state: () => ({
@@ -30,7 +33,7 @@ export const useCheckoutStore = defineStore('checkout', {
console.log("Allows write to PM: ", data.user.allows_write_to_pm);
if (! data.user.allows_write_to_pm) {
if (!data.user.allows_write_to_pm) {
console.log("Sending request");
const granted = await new Promise(resolve => {
window.Telegram.WebApp.requestWriteAccess((granted) => {
@@ -52,35 +55,44 @@ export const useCheckoutStore = defineStore('checkout', {
});
this.order = response.data;
if (! this.order.id) {
if (!this.order.id) {
console.debug(response.data);
throw new Error('Ошибка создания заказа.');
}
const yaMetrika = useYaMetrikaStore();
yaMetrika.reachGoal(YA_METRIKA_GOAL.ORDER_CREATED_SUCCESS, {
price: this.order?.final_total_numeric,
currency: this.order?.currency,
});
yaMetrika.dataLayerPush({
"ecommerce": {
"currencyCode": useSettingsStore().currency_code,
"purchase": {
"actionField": {
"id": this.order.id,
'revenue': this.order?.final_total_numeric,
},
"products": this.order.products ? this.order.products.map((product, index) => {
return {
id: product.product_id,
name: product.name,
price: product.total_numeric,
position: index,
quantity: product.quantity,
};
}) : [],
const pulse = usePulseStore();
await nextTick(() => {
yaMetrika.reachGoal(YA_METRIKA_GOAL.ORDER_CREATED_SUCCESS, {
price: this.order?.final_total_numeric,
currency: this.order?.currency,
});
yaMetrika.dataLayerPush({
"ecommerce": {
"currencyCode": useSettingsStore().currency_code,
"purchase": {
"actionField": {
"id": this.order.id,
'revenue': this.order?.final_total_numeric,
},
"products": this.order.products ? this.order.products.map((product, index) => {
return {
id: product.product_id,
name: product.name,
price: product.total_numeric,
position: index,
quantity: product.quantity,
};
}) : [],
}
}
}
});
pulse.ingest(TC_PULSE_EVENTS.ORDER_CREATED, {
order_id: this.order.id,
revenue: this.order?.final_total_numeric,
currency: this.order?.currency,
});
});
await window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');

View File

@@ -0,0 +1,50 @@
import {defineStore} from "pinia";
import {ingest, saveTelegramCustomer} from "@/utils/ftch.js";
import {toRaw} from "vue";
import {deserializeStartParams} from "@/helpers.js";
export const usePulseStore = defineStore('pulse', {
state: () => ({
tracking_id: null,
campaign_id: null,
}),
actions: {
initFromStartParams() {
const webapp = window.Telegram.WebApp;
const startParam = webapp.initDataUnsafe.start_param;
const deserialized = deserializeStartParams(startParam);
this.tracking_id = deserialized?.tracking_id;
this.campaign_id = deserialized?.campaign_id;
console.debug('[Pulse] Init with start parameters: ', deserialized);
},
ingest(event, eventData = {}) {
ingest({
event: event,
payload: {
webapp: window.Telegram.WebApp,
eventData: eventData,
},
})
.then(() => console.debug('[Pulse] Event Ingested', event, eventData))
.catch(err => console.error('Ingest failed:', err));
},
catchTelegramCustomerFromInitData() {
const userData = window.Telegram?.WebApp?.initDataUnsafe?.user;
if (userData) {
console.debug('[Pulse] Saving Telegram customer data');
saveTelegramCustomer(userData)
.then((response) => {
this.tracking_id = this.tracking_id || response?.data?.tracking_id || null;
console.debug(
'[Pulse] Telegram customer data saved successfully. Tracking ID: ',
toRaw(this.tracking_id)
);
})
.catch(() => console.warn('[Pulse] Failed to save Telegram customer data:', error));
}
},
},
});

View File

@@ -1,7 +1,7 @@
import {defineStore} from "pinia";
import {useSettingsStore} from "@/stores/SettingsStore.js";
import sha256 from 'crypto-js/sha256';
import {toRaw} from "vue";
import {usePulseStore} from "@/stores/Pulse.js";
export const useYaMetrikaStore = defineStore('ya_metrika', {
state: () => ({
@@ -20,6 +20,10 @@ export const useYaMetrikaStore = defineStore('ya_metrika', {
params.referer = params.referer ?? this.prevPath;
const pulse = usePulseStore();
params.campaign_id = params.campaign_id || pulse.campaign_id || null;
params.tracking_id = params.tracking_id || pulse.tracking_id || null;
if (typeof window.ym === 'function' && window.YA_METRIKA_ID !== undefined) {
console.debug('[ym] Hit ', fullUrl);
console.debug('[ym] ID ', window.YA_METRIKA_ID);
@@ -47,6 +51,10 @@ export const useYaMetrikaStore = defineStore('ya_metrika', {
return;
}
const pulse = usePulseStore();
params.campaign_id = params.campaign_id || pulse.campaign_id || null;
params.tracking_id = params.tracking_id || pulse.tracking_id || null;
if (typeof window.ym === 'function' && window.YA_METRIKA_ID !== undefined) {
console.debug('[ym] reachGoal ', target, ' params: ', params);
window.ym(window.YA_METRIKA_ID, 'reachGoal', target, params);
@@ -69,14 +77,8 @@ export const useYaMetrikaStore = defineStore('ya_metrika', {
}
if (typeof window.ym === 'function' && window.YA_METRIKA_ID !== undefined) {
let tgID = null;
if (window?.Telegram?.WebApp?.initDataUnsafe?.user?.id) {
tgID = sha256(window.Telegram.WebApp.initDataUnsafe.user.id).toString();
}
const userParams = {
tg_id: tgID,
tracking_id: usePulseStore().tracking_id,
language: window.Telegram?.WebApp?.initDataUnsafe?.user?.language_code || 'unknown',
platform: window.Telegram?.WebApp?.platform || 'unknown',
};
@@ -119,6 +121,19 @@ export const useYaMetrikaStore = defineStore('ya_metrika', {
return;
}
const pulse = usePulseStore();
const campaignId = pulse.campaign_id || null;
object.ecommerce = object.ecommerce || {};
if (campaignId) {
object.ecommerce.promotions = object.ecommerce.promotions || [];
object.ecommerce.promotions.push({ id: campaignId });
}
// Всегда добавляем ключи на верхнем уровне
object.campaign_id = campaignId;
object.tracking_id = pulse.tracking_id || null;
if (Array.isArray(window.dataLayer)) {
console.debug('[ym] dataLayer push: ', object);
window.dataLayer.push(object);