добавлена разметка событий аналитики

This commit is contained in:
kazachilo 2025-04-11 17:45:01 +03:00
parent 825be2d0a6
commit 2bf8cb05b2
22 changed files with 642 additions and 19 deletions

187
ANALYTICS_EVENTS.md Normal file
View File

@ -0,0 +1,187 @@
# Документация по аналитическим событиям
В этом документе представлен полный список событий для отслеживания в приложении с использованием сервиса `customAnalyticsService.ts`. События сгруппированы по экранам и функциональным блокам.
## 1. Общие события приложения
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Открытие мини-приложения | `app` | `app_open` | - | - |
## 2. Навигация
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Открытие экрана Главная | `navigation` | `view_home` | - | - |
| Открытие экрана Галерея | `navigation` | `view_gallery` | - | - |
| Открытие экрана Стикерпаки | `navigation` | `view_sticker_packs` | - | - |
| Открытие экрана Профиль | `navigation` | `view_profile` | - | - |
| Открытие экрана Обрезка фото | `navigation` | `view_crop_photo` | - | - |
| Открытие экрана Создание стикерпака | `navigation` | `view_create_sticker_pack` | - | - |
| Открытие экрана Добавление стикера в пак | `navigation` | `view_add_sticker_to_pack` | - | - |
| Открытие экрана Политика конфиденциальности | `navigation` | `view_terms_and_conditions` | - | - |
| Открытие экрана Инструкция | `navigation` | `view_how_to` | - | - |
## 3. Политика конфиденциальности
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Нажатие на ссылку политики | `terms` | `policy_link_click` | - | `link_url` |
| Принятие политики | `terms` | `accept` | - | - |
| Отклонение политики | `terms` | `decline` | - | - |
## 4. Главный экран (Home)
### 4.1 Верхний блок кнопок
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Нажатие на кнопку обратной связи | `ui_interaction` | `feedback_button_click` | - | - |
| Нажатие на кнопку инструкции | `ui_interaction` | `instruction_button_click` | - | - |
| Нажатие на кнопку другого бота | `ui_interaction` | `other_bot_button_click` | - | `bot_url` |
### 4.2 Хедер
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Нажатие на кнопку баланса | `ui_interaction` | `balance_button_click` | - | - |
### 4.3 Загрузка и обработка фото
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Нажатие на загрузить фото | `photo` | `upload_photo_click` | - | - |
| Применение обрезки фото | `photo` | `crop_photo_apply` | - | - |
### 4.4 Выбор стиля и параметров
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Нажатие кнопки Чиби стиль | `style` | `chibi_style_click` | - | - |
| Нажатие кнопки Эмодзи стиль | `style` | `emoji_style_click` | - | - |
| Выбор подкатегории Мем | `style` | `meme_subcategory_select` | - | `meme_id` |
| Выбор подкатегории Коллекция | `style` | `collection_subcategory_select` | - | `collection_id` |
### 4.5 Генерация стикера
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Удачная отправка на генерацию | `generation` | `generation_success` | 1 | `preset_name` |
| Неудачная отправка на генерацию | `generation` | `generation_failure` | - | `error_type` |
### 4.6 Футер
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Нажатие на кнопку Главная | `footer` | `home_button_click` | - | - |
| Нажатие на кнопку Галерея | `footer` | `gallery_button_click` | - | - |
| Нажатие на кнопку Стикерпаки | `footer` | `sticker_packs_button_click` | - | - |
| Нажатие на кнопку Профиль | `footer` | `profile_button_click` | - | - |
## 5. Экран Галерея
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Долгое удерживание на изображении | `gallery` | `image_long_press` | - | `image_id` |
| Удаление изображения | `gallery` | `image_delete` | - | `image_id` |
| Нажатие кнопки "Создать стикерпак" | `gallery` | `create_sticker_pack_click` | - | `from_gallery` |
## 6. Экран Стикерпаки
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Нажатие кнопки "Создать стикерпак" | `sticker_packs` | `create_sticker_pack_click` | - | `from_sticker_packs` |
| Создание стикерпака | `sticker_packs` | `sticker_pack_created` | - | `pack_url` |
| Ошибка создания стикерпака | `sticker_packs` | `sticker_pack_creation_error` | - | `error_type` |
| Удаление стикерпака | `sticker_packs` | `sticker_pack_deleted` | - | `pack_id` |
## 7. Экран Профиль и попап с офферами
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Нажатие на оффер на экране профиля | `offers` | `profile_offer_click` | - | `offer_id` |
| Нажатие на оффер в попапе | `offers` | `popup_offer_click` | - | `offer_id` |
| Успешная покупка | `payment` | `purchase_success` | `stars_amount` | `star` |
## 8. Экраны онбординга
### 8.1 Экран приветствия (OnboardingWelcome)
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Открытие экрана приветствия | `navigation` | `view_onboarding_welcome` | - | - |
| Нажатие кнопки "Далее" | `onboarding` | `welcome_next_click` | - | - |
| Нажатие кнопки "Пропустить" | `onboarding` | `welcome_skip_click` | - | - |
### 8.2 Экран инструкции (OnboardingHowTo)
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Открытие экрана инструкции | `navigation` | `view_onboarding_how_to` | - | - |
| Нажатие кнопки "Далее" | `onboarding` | `how_to_next_click` | - | - |
| Нажатие кнопки "Назад" | `onboarding` | `how_to_back_click` | - | - |
| Нажатие кнопки "Пропустить" | `onboarding` | `how_to_skip_click` | - | - |
| Переключение слайда инструкции | `onboarding` | `how_to_slide_change` | - | `slide_index` |
### 8.3 Экран стикерпаков (OnboardingStickerPacks)
| Событие | Категория | Название события | Значение | Единица измерения |
|---------|-----------|------------------|----------|-------------------|
| Открытие экрана стикерпаков | `navigation` | `view_onboarding_sticker_packs` | - | - |
| Нажатие кнопки "Начать" | `onboarding` | `sticker_packs_start_click` | - | - |
| Нажатие кнопки "Назад" | `onboarding` | `sticker_packs_back_click` | - | - |
## Примеры использования
### Отслеживание открытия приложения
```typescript
import customAnalyticsService from '../services/customAnalyticsService';
// В компоненте App.tsx при монтировании
useEffect(() => {
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'app',
event_name: 'app_open'
});
}, []);
```
### Отслеживание навигации
```typescript
import customAnalyticsService from '../services/customAnalyticsService';
// В компоненте страницы
useEffect(() => {
customAnalyticsService.trackNavigation('home');
}, []);
```
### Отслеживание генерации стикера
```typescript
import customAnalyticsService from '../services/customAnalyticsService';
// При успешной генерации стикера
const handleGenerationSuccess = (presetName) => {
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'generation',
event_name: 'generation_success',
value: 1,
unit: presetName
});
};
```
### Отслеживание покупки
```typescript
import customAnalyticsService from '../services/customAnalyticsService';
// При успешной покупке
const handlePurchaseSuccess = (starsAmount) => {
customAnalyticsService.trackPayment('purchase_success', starsAmount, 'star');
};

View File

@ -2,8 +2,9 @@ import React, { lazy, Suspense, useEffect } from 'react';
import { BrowserRouter, Routes, Route, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom';
import Layout from './components/layout/Layout'; import Layout from './components/layout/Layout';
import Home from './screens/Home'; import Home from './screens/Home';
import { initializeUserId } from './constants/user'; import { initializeUserId, getCurrentUserId } from './constants/user';
import { trackScreenView } from './services/analyticsService'; import { trackScreenView } from './services/analyticsService';
import customAnalyticsService from './services/customAnalyticsService';
import { BalanceProvider } from './contexts/BalanceContext'; import { BalanceProvider } from './contexts/BalanceContext';
// Ленивая загрузка компонентов // Ленивая загрузка компонентов
@ -39,9 +40,18 @@ const AppContent: React.FC = () => {
useEffect(() => { useEffect(() => {
// Инициализируем ID пользователя при запуске приложения // Инициализируем ID пользователя при запуске приложения
initializeUserId().catch(error => { initializeUserId()
console.error('Ошибка при инициализации пользователя:', error); .then(() => {
}); // Отправляем событие открытия приложения после успешной инициализации
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'app',
event_name: 'app_open'
});
})
.catch(error => {
console.error('Ошибка при инициализации пользователя:', error);
});
// Стабилизируем окно и отключаем вертикальные свайпы // Стабилизируем окно и отключаем вертикальные свайпы
if (window.Telegram?.WebApp) { if (window.Telegram?.WebApp) {
@ -98,6 +108,9 @@ const AppContent: React.FC = () => {
// Отправляем событие просмотра экрана // Отправляем событие просмотра экрана
trackScreenView(screenName); trackScreenView(screenName);
// Отправляем событие навигации в нашу новую аналитику
customAnalyticsService.trackNavigation(location.pathname.replace('/', ''));
}, [location.pathname]); }, [location.pathname]);
return ( return (

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import BlockRenderer from '../blocks/BlockRenderer'; import BlockRenderer from '../blocks/BlockRenderer';
import { homeScreenConfig } from '../../config/homeScreen'; import { homeScreenConfig } from '../../config/homeScreen';
import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user';
interface EmotionTypeSelectorProps { interface EmotionTypeSelectorProps {
selectedEmotionTypeButtonId?: string; selectedEmotionTypeButtonId?: string;
@ -30,6 +32,14 @@ const EmotionTypeSelector: React.FC<EmotionTypeSelectorProps> = ({
// Обработчик выбора типа эмоций // Обработчик выбора типа эмоций
const handleAction = (actionType: string, actionValue: string, blockId?: string, buttonId?: string) => { const handleAction = (actionType: string, actionValue: string, blockId?: string, buttonId?: string) => {
if (actionType === 'selectEmotionType') { if (actionType === 'selectEmotionType') {
// Отслеживаем событие выбора типа эмоций
const eventName = actionValue === 'memes' ? 'memes_category_click' : 'prompts_category_click';
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'emotion_type',
event_name: eventName
});
onEmotionTypeSelect(actionValue as 'memes' | 'prompts', buttonId); onEmotionTypeSelect(actionValue as 'memes' | 'prompts', buttonId);
} }
}; };

View File

@ -2,6 +2,8 @@ import React from 'react';
import BlockRenderer from '../blocks/BlockRenderer'; import BlockRenderer from '../blocks/BlockRenderer';
import { homeScreenConfig } from '../../config/homeScreen'; import { homeScreenConfig } from '../../config/homeScreen';
import styles from '../../screens/Home.module.css'; import styles from '../../screens/Home.module.css';
import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user';
interface GenerationButtonProps { interface GenerationButtonProps {
onStartGeneration: () => void; onStartGeneration: () => void;
@ -25,6 +27,13 @@ const GenerationButton: React.FC<GenerationButtonProps> = ({
// Обработчик нажатия на кнопку генерации // Обработчик нажатия на кнопку генерации
const handleAction = (actionType: string, actionValue: string) => { const handleAction = (actionType: string, actionValue: string) => {
if (actionType === 'function' && actionValue === 'startGeneration' && !isGenerating) { if (actionType === 'function' && actionValue === 'startGeneration' && !isGenerating) {
// Отслеживаем событие нажатия на кнопку генерации
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'generation',
event_name: 'generate_button_click'
});
onStartGeneration(); onStartGeneration();
} }
}; };

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import BlockRenderer from '../blocks/BlockRenderer'; import BlockRenderer from '../blocks/BlockRenderer';
import { homeScreenConfig } from '../../config/homeScreen'; import { homeScreenConfig } from '../../config/homeScreen';
import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user';
interface MainActionsProps { interface MainActionsProps {
onSendFeedback: () => void; onSendFeedback: () => void;
@ -26,8 +28,21 @@ const MainActions: React.FC<MainActionsProps> = ({
const handleAction = (actionType: string, actionValue: string) => { const handleAction = (actionType: string, actionValue: string) => {
if (actionType === 'function') { if (actionType === 'function') {
if (actionValue === 'sendFeedback') { if (actionValue === 'sendFeedback') {
// Отслеживаем событие нажатия на кнопку обратной связи
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'ui_interaction',
event_name: 'feedback_button_click'
});
onSendFeedback(); onSendFeedback();
} else if (actionValue === 'openTelegramBot') { } else if (actionValue === 'openTelegramBot') {
// Отслеживаем событие нажатия на кнопку другого бота
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'ui_interaction',
event_name: 'other_bot_button_click',
unit: 'https://t.me/youtube_s_loader_bot'
});
onOpenTelegramBot(); onOpenTelegramBot();
} }
} }

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import BlockRenderer from '../blocks/BlockRenderer'; import BlockRenderer from '../blocks/BlockRenderer';
import { homeScreenConfig } from '../../config/homeScreen'; import { homeScreenConfig } from '../../config/homeScreen';
import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user';
interface PhotoUploadProps { interface PhotoUploadProps {
onImageDataChange: (imageData: string) => void; onImageDataChange: (imageData: string) => void;
@ -22,8 +24,17 @@ const PhotoUpload: React.FC<PhotoUploadProps> = ({
// Обработчик загрузки фото // Обработчик загрузки фото
const handleAction = (actionType: string, actionValue: string, blockId?: string, buttonId?: string, extraData?: any) => { const handleAction = (actionType: string, actionValue: string, blockId?: string, buttonId?: string, extraData?: any) => {
if (actionType === 'uploadPhoto' && extraData?.imageData) { if (actionType === 'uploadPhoto') {
onImageDataChange(extraData.imageData); // Отслеживаем событие нажатия на загрузить фото
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'photo',
event_name: 'upload_photo_click'
});
if (extraData?.imageData) {
onImageDataChange(extraData.imageData);
}
} }
}; };

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import BlockRenderer from '../blocks/BlockRenderer'; import BlockRenderer from '../blocks/BlockRenderer';
import { homeScreenConfig } from '../../config/homeScreen'; import { homeScreenConfig } from '../../config/homeScreen';
import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user';
interface StyleSelectorProps { interface StyleSelectorProps {
selectedStyleButtonId?: string; selectedStyleButtonId?: string;
@ -25,6 +27,14 @@ const StyleSelector: React.FC<StyleSelectorProps> = ({
// Обработчик выбора стиля // Обработчик выбора стиля
const handleAction = (actionType: string, actionValue: string, blockId?: string, buttonId?: string) => { const handleAction = (actionType: string, actionValue: string, blockId?: string, buttonId?: string) => {
if (actionType === 'selectStyle') { if (actionType === 'selectStyle') {
// Отслеживаем событие выбора стиля
const eventName = actionValue === 'chibi' ? 'chibi_style_click' : 'emoji_style_click';
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'style',
event_name: eventName
});
onStyleSelect(actionValue, buttonId); onStyleSelect(actionValue, buttonId);
} }
}; };

View File

@ -8,6 +8,7 @@ import { paymentService } from '../../services/paymentService';
import { tokenPacks } from '../../constants/tokenPacks'; import { tokenPacks } from '../../constants/tokenPacks';
import NotificationModal from '../shared/NotificationModal'; import NotificationModal from '../shared/NotificationModal';
import { useBalance } from '../../contexts/BalanceContext'; import { useBalance } from '../../contexts/BalanceContext';
import customAnalyticsService from '../../services/customAnalyticsService';
import styles from './Header.module.css'; import styles from './Header.module.css';
const Header: React.FC = () => { const Header: React.FC = () => {
@ -70,7 +71,15 @@ const Header: React.FC = () => {
{/* Баланс токенов */} {/* Баланс токенов */}
<button <button
className={styles.balance} className={styles.balance}
onClick={() => setShowTokensModal(true)} onClick={() => {
// Отслеживаем событие нажатия на кнопку баланса
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'ui_interaction',
event_name: 'balance_button_click'
});
setShowTokensModal(true);
}}
title="Нажмите чтобы пополнить баланс" title="Нажмите чтобы пополнить баланс"
> >
<span className={styles.balanceIcon}> <span className={styles.balanceIcon}>

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import styles from './Navigation.module.css'; import styles from './Navigation.module.css';
import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user';
const NavigationComponent: React.FC = () => { const NavigationComponent: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -13,7 +15,15 @@ const NavigationComponent: React.FC = () => {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.list}> <div className={styles.list}>
<button <button
onClick={() => navigate('/')} onClick={() => {
// Отслеживаем событие нажатия на кнопку Главная
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'footer',
event_name: 'home_button_click'
});
navigate('/');
}}
className={`${styles.item} ${isActive('/') ? styles.active : ''}`} className={`${styles.item} ${isActive('/') ? styles.active : ''}`}
> >
<span className={styles.icon}> <span className={styles.icon}>
@ -26,7 +36,15 @@ const NavigationComponent: React.FC = () => {
</button> </button>
<button <button
onClick={() => navigate('/gallery')} onClick={() => {
// Отслеживаем событие нажатия на кнопку Галерея
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'footer',
event_name: 'gallery_button_click'
});
navigate('/gallery');
}}
className={`${styles.item} ${isActive('/gallery') ? styles.active : ''}`} className={`${styles.item} ${isActive('/gallery') ? styles.active : ''}`}
> >
<span className={styles.icon}> <span className={styles.icon}>
@ -40,7 +58,15 @@ const NavigationComponent: React.FC = () => {
</button> </button>
<button <button
onClick={() => navigate('/packs')} onClick={() => {
// Отслеживаем событие нажатия на кнопку Стикерпаки
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'footer',
event_name: 'sticker_packs_button_click'
});
navigate('/packs');
}}
className={`${styles.item} ${isActive('/packs') ? styles.active : ''}`} className={`${styles.item} ${isActive('/packs') ? styles.active : ''}`}
> >
<span className={styles.icon}> <span className={styles.icon}>
@ -53,7 +79,15 @@ const NavigationComponent: React.FC = () => {
</button> </button>
<button <button
onClick={() => navigate('/profile')} onClick={() => {
// Отслеживаем событие нажатия на кнопку Профиль
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'footer',
event_name: 'profile_button_click'
});
navigate('/profile');
}}
className={`${styles.item} ${isActive('/profile') ? styles.active : ''}`} className={`${styles.item} ${isActive('/profile') ? styles.active : ''}`}
> >
<span className={styles.icon}> <span className={styles.icon}>

View File

@ -1,12 +1,15 @@
import React from 'react'; import React from 'react';
import { images } from '../../assets'; import { images } from '../../assets';
import { TokenPack } from '../../constants/tokenPacks'; import { TokenPack } from '../../constants/tokenPacks';
import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user';
import styles from './TokenPackCard.module.css'; import styles from './TokenPackCard.module.css';
interface TokenPackCardProps extends TokenPack { interface TokenPackCardProps extends TokenPack {
onBuy: () => void; onBuy: () => void;
className?: string; className?: string;
compact?: boolean; compact?: boolean;
source?: 'popup' | 'profile'; // Источник отображения карточки
} }
const TokenPackCard: React.FC<TokenPackCardProps> = ({ const TokenPackCard: React.FC<TokenPackCardProps> = ({
@ -20,12 +23,22 @@ const TokenPackCard: React.FC<TokenPackCardProps> = ({
isPopular, isPopular,
isBestValue, isBestValue,
onBuy, onBuy,
className = '' className = '',
source = 'popup' // По умолчанию считаем, что карточка отображается в попапе
}) => { }) => {
return ( return (
<div <div
className={`${styles.card} ${isPopular ? styles.popular : ''} ${isBestValue ? styles.bestValue : ''} ${className}`} className={`${styles.card} ${isPopular ? styles.popular : ''} ${isBestValue ? styles.bestValue : ''} ${className}`}
onClick={onBuy} // Добавляем обработчик клика на всю карточку onClick={() => {
// Отслеживаем событие нажатия на оффер
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'offers',
event_name: source === 'popup' ? 'popup_offer_click' : 'profile_offer_click',
unit: title
});
onBuy();
}} // Добавляем обработчик клика на всю карточку
style={{ cursor: 'pointer' }} // Добавляем стиль курсора, чтобы показать, что карточка кликабельна style={{ cursor: 'pointer' }} // Добавляем стиль курсора, чтобы показать, что карточка кликабельна
> >
{isPopular && ( {isPopular && (
@ -67,6 +80,13 @@ const TokenPackCard: React.FC<TokenPackCardProps> = ({
className={styles.buyButton} className={styles.buyButton}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); // Предотвращаем всплытие события e.stopPropagation(); // Предотвращаем всплытие события
// Отслеживаем событие нажатия на кнопку "КУПИТЬ"
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'offers',
event_name: source === 'popup' ? 'popup_offer_click' : 'profile_offer_click',
unit: title
});
onBuy(); onBuy();
}} }}
> >

View File

@ -7,12 +7,14 @@ interface TokenPacksListProps {
onBuyPack: (packId: string) => void; onBuyPack: (packId: string) => void;
className?: string; className?: string;
compact?: boolean; compact?: boolean;
source?: 'popup' | 'profile'; // Источник отображения списка
} }
const TokenPacksList: React.FC<TokenPacksListProps> = ({ const TokenPacksList: React.FC<TokenPacksListProps> = ({
onBuyPack, onBuyPack,
className = '', className = '',
compact compact,
source = 'popup' // По умолчанию считаем, что список отображается в попапе
}) => { }) => {
const listRef = useRef<HTMLDivElement>(null); const listRef = useRef<HTMLDivElement>(null);
@ -40,6 +42,7 @@ const TokenPacksList: React.FC<TokenPacksListProps> = ({
key={pack.id} key={pack.id}
{...pack} {...pack}
compact={compact} compact={compact}
source={source}
onBuy={() => onBuyPack(pack.id)} onBuy={() => onBuyPack(pack.id)}
/> />
))} ))}

View File

@ -5,6 +5,8 @@ import { checkSufficientBalance, updateBalanceWithRetries } from '../utils/balan
import apiService from '../services/api'; import apiService from '../services/api';
import { useBalance } from '../contexts/BalanceContext'; import { useBalance } from '../contexts/BalanceContext';
import { WorkflowType } from '../constants/workflows'; import { WorkflowType } from '../constants/workflows';
import customAnalyticsService from '../services/customAnalyticsService';
import { getCurrentUserId } from '../constants/user';
/** /**
* Хук для управления состоянием генерации * Хук для управления состоянием генерации
@ -232,6 +234,14 @@ export const useGenerationState = (
// Устанавливаем таймер для обнаружения проблем с подключением (6 секунд) // Устанавливаем таймер для обнаружения проблем с подключением (6 секунд)
connectionTimeoutId = setTimeout(() => { connectionTimeoutId = setTimeout(() => {
console.log('Connection timeout triggered'); console.log('Connection timeout triggered');
// Отслеживаем событие проблем с подключением
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'generation',
event_name: 'connection_timeout'
});
// Вызываем handleRequestTimeout из useNotifications через showNotification // Вызываем handleRequestTimeout из useNotifications через showNotification
showNotification( showNotification(
'Генерация стикера', 'Генерация стикера',
@ -318,6 +328,15 @@ export const useGenerationState = (
// Получаем результат // Получаем результат
const { result } = response; const { result } = response;
// Отслеживаем событие успешной отправки на генерацию
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'generation',
event_name: 'generation_request_success',
value: 1,
unit: workflowType
});
// Обновляем уведомление с информацией о позиции в очереди // Обновляем уведомление с информацией о позиции в очереди
if (result.queue_position !== undefined) { if (result.queue_position !== undefined) {
const estimatedTime = apiService.calculateEstimatedWaitTime(result.queue_position); const estimatedTime = apiService.calculateEstimatedWaitTime(result.queue_position);

View File

@ -7,6 +7,7 @@ import { GeneratedImage } from '../types/api';
import { getCurrentUserId } from '../constants/user'; import { getCurrentUserId } from '../constants/user';
import EmojiPickerModal from '../components/shared/EmojiPickerModal'; import EmojiPickerModal from '../components/shared/EmojiPickerModal';
import ValidationModal from '../components/shared/ValidationModal'; import ValidationModal from '../components/shared/ValidationModal';
import customAnalyticsService from '../services/customAnalyticsService';
/** /**
* Транслитерирует кириллический текст в латиницу. * Транслитерирует кириллический текст в латиницу.
@ -200,6 +201,14 @@ const CreateStickerPack: React.FC = () => {
emojis, emojis,
packName packName
); );
// Отслеживаем событие успешного создания стикерпака
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'sticker_packs',
event_name: 'sticker_pack_created',
unit: packName
});
// Переходим на страницу стикерпаков // Переходим на страницу стикерпаков
navigate('/packs'); navigate('/packs');
@ -209,6 +218,14 @@ const CreateStickerPack: React.FC = () => {
// Преобразуем ошибку в строку для поиска // Преобразуем ошибку в строку для поиска
const errorMessage = err instanceof Error ? err.message : String(err); const errorMessage = err instanceof Error ? err.message : String(err);
// Отслеживаем событие ошибки создания стикерпака
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'sticker_packs',
event_name: 'sticker_pack_creation_error',
unit: errorMessage
});
// Проверяем, содержит ли сообщение об ошибке информацию о занятом имени // Проверяем, содержит ли сообщение об ошибке информацию о занятом имени
if (errorMessage.includes('sticker set name is already occupied')) { if (errorMessage.includes('sticker set name is already occupied')) {
setValidationTitle('Имя стикерпака уже занято'); setValidationTitle('Имя стикерпака уже занято');

View File

@ -6,6 +6,8 @@ import { GeneratedImage, PendingTask } from '../types/api';
import ImageViewer from '../components/shared/ImageViewer'; import ImageViewer from '../components/shared/ImageViewer';
import ImageWithFallback from '../components/shared/ImageWithFallback'; import ImageWithFallback from '../components/shared/ImageWithFallback';
import NotificationModal from '../components/shared/NotificationModal'; import NotificationModal from '../components/shared/NotificationModal';
import customAnalyticsService from '../services/customAnalyticsService';
import { getCurrentUserId } from '../constants/user';
const GalleryScreen: React.FC = () => { const GalleryScreen: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -38,6 +40,14 @@ const GalleryScreen: React.FC = () => {
// Обработчики для режима удаления // Обработчики для режима удаления
const handleLongPress = useCallback((image: GeneratedImage) => { const handleLongPress = useCallback((image: GeneratedImage) => {
// Отслеживаем событие долгого нажатия на изображение
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'gallery',
event_name: 'image_long_press',
unit: String(image.id)
});
setIsDeleteMode(true); setIsDeleteMode(true);
}, []); }, []);
@ -64,6 +74,14 @@ const GalleryScreen: React.FC = () => {
setIsDeleting(true); setIsDeleting(true);
await apiService.deleteImage(selectedForDelete.link); await apiService.deleteImage(selectedForDelete.link);
// Отслеживаем событие удаления изображения
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'gallery',
event_name: 'image_delete',
unit: String(selectedForDelete.id)
});
// Обновляем список изображений // Обновляем список изображений
setImages(prevImages => setImages(prevImages =>
prevImages.filter(img => img.id !== selectedForDelete.id) prevImages.filter(img => img.id !== selectedForDelete.id)
@ -85,6 +103,14 @@ const GalleryScreen: React.FC = () => {
// Обработчик для кнопки "Создать стикерпак" // Обработчик для кнопки "Создать стикерпак"
const handleCreateStickerPack = useCallback(() => { const handleCreateStickerPack = useCallback(() => {
// Отслеживаем событие нажатия на кнопку "Создать стикерпак"
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'gallery',
event_name: 'create_sticker_pack_click',
unit: 'from_gallery'
});
navigate('/create-sticker-pack'); navigate('/create-sticker-pack');
}, [navigate]); }, [navigate]);

View File

@ -105,6 +105,7 @@ const Profile: React.FC = () => {
<TokenPacksList <TokenPacksList
onBuyPack={handleBuyPack} onBuyPack={handleBuyPack}
className={styles.tokenPacks} className={styles.tokenPacks}
source="profile"
/> />
{/* Модальное окно успешной оплаты */} {/* Модальное окно успешной оплаты */}

View File

@ -4,6 +4,7 @@ import styles from './StickerPacks.module.css';
import { stickerService } from '../services/stickerService'; import { stickerService } from '../services/stickerService';
import { getCurrentUserId } from '../constants/user'; import { getCurrentUserId } from '../constants/user';
import NotificationModal from '../components/shared/NotificationModal'; import NotificationModal from '../components/shared/NotificationModal';
import customAnalyticsService from '../services/customAnalyticsService';
// Функция для удаления дописанной части из названия стикерпака // Функция для удаления дописанной части из названия стикерпака
const cleanPackTitle = (title: string): string => { const cleanPackTitle = (title: string): string => {
@ -70,6 +71,14 @@ const StickerPacks: React.FC = () => {
}, []); }, []);
const handleCreateStickerPack = () => { const handleCreateStickerPack = () => {
// Отслеживаем событие нажатия на кнопку "Создать стикерпак"
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'sticker_packs',
event_name: 'create_sticker_pack_click',
unit: 'from_sticker_packs'
});
navigate('/create-sticker-pack'); navigate('/create-sticker-pack');
}; };
@ -90,6 +99,15 @@ const StickerPacks: React.FC = () => {
try { try {
setIsDeleting(true); setIsDeleting(true);
await stickerService.deleteStickerPack(packToDelete); await stickerService.deleteStickerPack(packToDelete);
// Отслеживаем событие удаления стикерпака
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'sticker_packs',
event_name: 'sticker_pack_deleted',
unit: packToDelete
});
setStickerPacks(prevPacks => prevPacks.filter(pack => pack.name !== packToDelete)); setStickerPacks(prevPacks => prevPacks.filter(pack => pack.name !== packToDelete));
setSelectedPack(null); setSelectedPack(null);
setIsDeleting(false); setIsDeleting(false);

View File

@ -1,16 +1,41 @@
import React from 'react'; import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import OnboardingLayout from '../../components/shared/OnboardingLayout'; import OnboardingLayout from '../../components/shared/OnboardingLayout';
import styles from './OnboardingHowTo.module.css'; import styles from './OnboardingHowTo.module.css';
import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user';
const OnboardingHowTo: React.FC = () => { const OnboardingHowTo: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => {
// Отслеживаем событие открытия экрана инструкции
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'navigation',
event_name: 'view_onboarding_how_to'
});
}, []);
const handleNext = () => { const handleNext = () => {
// Отслеживаем событие нажатия кнопки "Далее"
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'onboarding',
event_name: 'how_to_next_click'
});
navigate('/onboarding/sticker-packs'); navigate('/onboarding/sticker-packs');
}; };
const handleSkip = () => { const handleSkip = () => {
// Отслеживаем событие нажатия кнопки "Пропустить"
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'onboarding',
event_name: 'how_to_skip_click'
});
localStorage.setItem('hasSeenOnboarding', 'true'); localStorage.setItem('hasSeenOnboarding', 'true');
navigate('/'); navigate('/');
}; };

View File

@ -1,12 +1,30 @@
import React from 'react'; import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import OnboardingLayout from '../../components/shared/OnboardingLayout'; import OnboardingLayout from '../../components/shared/OnboardingLayout';
import styles from './OnboardingStickerPacks.module.css'; import styles from './OnboardingStickerPacks.module.css';
import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user';
const OnboardingStickerPacks: React.FC = () => { const OnboardingStickerPacks: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => {
// Отслеживаем событие открытия экрана стикерпаков
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'navigation',
event_name: 'view_onboarding_sticker_packs'
});
}, []);
const handleStart = () => { const handleStart = () => {
// Отслеживаем событие нажатия кнопки "Начать"
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'onboarding',
event_name: 'sticker_packs_start_click'
});
// Устанавливаем флаг, что пользователь видел онбординг // Устанавливаем флаг, что пользователь видел онбординг
localStorage.setItem('hasSeenOnboarding', 'true'); localStorage.setItem('hasSeenOnboarding', 'true');

View File

@ -1,16 +1,41 @@
import React from 'react'; import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import OnboardingLayout from '../../components/shared/OnboardingLayout'; import OnboardingLayout from '../../components/shared/OnboardingLayout';
import { images } from '../../assets'; import { images } from '../../assets';
import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user';
const OnboardingWelcome: React.FC = () => { const OnboardingWelcome: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => {
// Отслеживаем событие открытия экрана приветствия
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'navigation',
event_name: 'view_onboarding_welcome'
});
}, []);
const handleNext = () => { const handleNext = () => {
// Отслеживаем событие нажатия кнопки "Далее"
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'onboarding',
event_name: 'welcome_next_click'
});
navigate('/onboarding/how-to'); navigate('/onboarding/how-to');
}; };
const handleSkip = () => { const handleSkip = () => {
// Отслеживаем событие нажатия кнопки "Пропустить"
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'onboarding',
event_name: 'welcome_skip_click'
});
localStorage.setItem('hasSeenOnboarding', 'true'); localStorage.setItem('hasSeenOnboarding', 'true');
navigate('/'); navigate('/');
}; };

View File

@ -1,12 +1,30 @@
import React from 'react'; import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import styles from './TermsAndConditions.module.css'; import styles from './TermsAndConditions.module.css';
import { images } from '../../assets'; import { images } from '../../assets';
import customAnalyticsService from '../../services/customAnalyticsService';
import { getCurrentUserId } from '../../constants/user';
const TermsAndConditions: React.FC = () => { const TermsAndConditions: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => {
// Отслеживаем событие открытия экрана условий
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'navigation',
event_name: 'view_terms_and_conditions'
});
}, []);
const handleAccept = () => { const handleAccept = () => {
// Отслеживаем событие принятия условий
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'terms',
event_name: 'accept'
});
// Устанавливаем флаги, что пользователь видел онбординг и принял условия // Устанавливаем флаги, что пользователь видел онбординг и принял условия
localStorage.setItem('hasSeenOnboarding', 'true'); localStorage.setItem('hasSeenOnboarding', 'true');
localStorage.setItem('hasAcceptedTerms', 'true'); localStorage.setItem('hasAcceptedTerms', 'true');
@ -15,11 +33,28 @@ const TermsAndConditions: React.FC = () => {
}; };
const handleDecline = () => { const handleDecline = () => {
// Отслеживаем событие отклонения условий
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'terms',
event_name: 'decline'
});
// Закрываем приложение // Закрываем приложение
if (window.Telegram?.WebApp?.close) { if (window.Telegram?.WebApp?.close) {
window.Telegram.WebApp.close(); window.Telegram.WebApp.close();
} }
}; };
const handlePolicyLinkClick = (linkUrl: string) => {
// Отслеживаем событие нажатия на ссылку политики
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'terms',
event_name: 'policy_link_click',
unit: linkUrl
});
};
return ( return (
<div className={styles.container}> <div className={styles.container}>
@ -40,6 +75,7 @@ const TermsAndConditions: React.FC = () => {
className={styles.link} className={styles.link}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={() => handlePolicyLinkClick("https://telegra.ph/Polzovatelskoe-soglashenie-03-19-13")}
> >
Условия использования Условия использования
</a> </a>
@ -48,6 +84,7 @@ const TermsAndConditions: React.FC = () => {
className={styles.link} className={styles.link}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={() => handlePolicyLinkClick("https://telegra.ph/Politika-konfidencialnosti-03-19-10")}
> >
Политика конфиденциальности Политика конфиденциальности
</a> </a>

View File

@ -0,0 +1,107 @@
import { getCurrentUserId } from '../constants/user';
// Базовый URL API
const API_BASE_URL = 'https://stickerserver.gymnasticstuff.uk';
// Интерфейс для данных события
export interface EventData {
telegram_id: number;
event_category: string;
event_name: string;
value?: number;
unit?: string;
}
/**
* Отправляет событие аналитики на сервер
* @param eventData Данные события
* @returns Promise<boolean> Успешность операции
*/
export const trackEvent = async (eventData: EventData): Promise<boolean> => {
try {
const response = await fetch(`${API_BASE_URL}/events/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'accept': 'application/json',
},
body: JSON.stringify(eventData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('Событие аналитики успешно отправлено:', result);
return true;
} catch (error) {
console.error('Ошибка при отправке события аналитики:', error);
return false;
}
};
/**
* Отслеживает событие использования функционала
* @param eventName Название события
* @param value Числовое значение (опционально)
* @param unit Единица измерения (опционально)
* @returns Promise<boolean> Успешность операции
*/
export const trackUsage = async (
eventName: string,
value?: number,
unit?: string
): Promise<boolean> => {
return trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'usage',
event_name: eventName,
value,
unit
});
};
/**
* Отслеживает событие платежа
* @param eventName Название события
* @param amount Сумма платежа
* @param currency Валюта платежа (например, 'stars', 'USD')
* @returns Promise<boolean> Успешность операции
*/
export const trackPayment = async (
eventName: string,
amount: number,
currency: string
): Promise<boolean> => {
return trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'payment',
event_name: eventName,
value: amount,
unit: currency
});
};
/**
* Отслеживает событие навигации по приложению
* @param screenName Название экрана
* @returns Promise<boolean> Успешность операции
*/
export const trackNavigation = async (screenName: string): Promise<boolean> => {
return trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'navigation',
event_name: `view_${screenName}`
});
};
// Экспортируем все функции по умолчанию
const customAnalyticsService = {
trackEvent,
trackUsage,
trackPayment,
trackNavigation
};
export default customAnalyticsService;

View File

@ -1,7 +1,7 @@
import { TokenPack } from '../constants/tokenPacks'; import { TokenPack } from '../constants/tokenPacks';
import apiService from '../services/api'; import apiService from '../services/api';
import { getCurrentUserId } from '../constants/user'; import { getCurrentUserId } from '../constants/user';
import { sendTargetEvent } from './analyticsService'; import customAnalyticsService from './customAnalyticsService';
export const paymentService = { export const paymentService = {
showBuyTokensPopup: async (pack: TokenPack, onSuccess?: (userData?: any) => void) => { showBuyTokensPopup: async (pack: TokenPack, onSuccess?: (userData?: any) => void) => {
@ -25,6 +25,15 @@ export const paymentService = {
// Открываем встроенный платеж Telegram без предварительного подтверждения // Открываем встроенный платеж Telegram без предварительного подтверждения
webApp.openInvoice(invoiceLink, async (status: 'paid' | 'cancelled' | 'failed' | 'pending') => { webApp.openInvoice(invoiceLink, async (status: 'paid' | 'cancelled' | 'failed' | 'pending') => {
if (status === 'paid') { if (status === 'paid') {
// Отслеживаем событие успешной покупки
customAnalyticsService.trackEvent({
telegram_id: getCurrentUserId(),
event_category: 'payment',
event_name: 'purchase_success',
value: pack.tokens + pack.bonusTokens,
unit: 'star'
});
// Функция для выполнения одной попытки получения данных пользователя // Функция для выполнения одной попытки получения данных пользователя
const fetchUserData = async (attempt: number) => { const fetchUserData = async (attempt: number) => {
try { try {