636 lines
18 KiB
Markdown
636 lines
18 KiB
Markdown
# Challenge Service - Аналитика и метрики для фронтенда
|
||
|
||
Краткое руководство по ключевым метрикам и аналитике для интеграции на фронтенде.
|
||
|
||
## 📊 Ключевые метрики для отслеживания
|
||
|
||
### 1. Метрики производительности
|
||
|
||
```typescript
|
||
// Метрики для мониторинга
|
||
interface PerformanceMetrics {
|
||
// Время от отправки до получения результата
|
||
timeToFeedback: number // миллисекунды
|
||
|
||
// Время ожидания в очереди
|
||
queueWaitTime: number // миллисекунды
|
||
|
||
// Время непосредственной проверки
|
||
checkTime: number // миллисекунды
|
||
|
||
// Позиция в очереди при добавлении
|
||
initialQueuePosition: number
|
||
|
||
// Количество проверок статуса до завершения
|
||
pollsBeforeComplete: number
|
||
}
|
||
|
||
// Пример сбора метрик
|
||
class MetricsCollector {
|
||
private startTime: number = 0
|
||
private pollCount: number = 0
|
||
|
||
startTracking() {
|
||
this.startTime = Date.now()
|
||
this.pollCount = 0
|
||
}
|
||
|
||
incrementPoll() {
|
||
this.pollCount++
|
||
}
|
||
|
||
getMetrics(submission: ChallengeSubmission): PerformanceMetrics {
|
||
return {
|
||
timeToFeedback: Date.now() - this.startTime,
|
||
queueWaitTime: submission.checkedAt
|
||
? new Date(submission.checkedAt).getTime() - new Date(submission.submittedAt).getTime()
|
||
: 0,
|
||
checkTime: submission.checkedAt
|
||
? new Date(submission.checkedAt).getTime() - new Date(submission.submittedAt).getTime()
|
||
: 0,
|
||
initialQueuePosition: 0, // Сохранить из первого ответа
|
||
pollsBeforeComplete: this.pollCount
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. Метрики пользовательского поведения
|
||
|
||
```typescript
|
||
interface UserBehaviorMetrics {
|
||
// Время, проведенное на задании
|
||
timeSpentOnTask: number // секунды
|
||
|
||
// Количество символов в решении
|
||
solutionLength: number
|
||
|
||
// Количество редактирований текста
|
||
editCount: number
|
||
|
||
// Использовал ли черновик
|
||
usedDraft: boolean
|
||
|
||
// Время от загрузки до отправки
|
||
timeToSubmit: number // секунды
|
||
}
|
||
|
||
// Трекинг поведения
|
||
class BehaviorTracker {
|
||
private taskStartTime: number = Date.now()
|
||
private editCount: number = 0
|
||
private lastValue: string = ''
|
||
|
||
onTextChange(newValue: string) {
|
||
if (newValue !== this.lastValue) {
|
||
this.editCount++
|
||
this.lastValue = newValue
|
||
}
|
||
}
|
||
|
||
getMetrics(result: string, usedDraft: boolean): UserBehaviorMetrics {
|
||
return {
|
||
timeSpentOnTask: Math.floor((Date.now() - this.taskStartTime) / 1000),
|
||
solutionLength: result.length,
|
||
editCount: this.editCount,
|
||
usedDraft,
|
||
timeToSubmit: Math.floor((Date.now() - this.taskStartTime) / 1000)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. Метрики успешности
|
||
|
||
```typescript
|
||
interface SuccessMetrics {
|
||
// Процент принятых заданий с первой попытки
|
||
firstAttemptSuccessRate: number // 0-100
|
||
|
||
// Среднее количество попыток до успеха
|
||
averageAttemptsToSuccess: number
|
||
|
||
// Процент завершенных цепочек
|
||
chainCompletionRate: number // 0-100
|
||
|
||
// Время до первого успешного задания
|
||
timeToFirstSuccess: number // минуты
|
||
}
|
||
|
||
// Расчет метрик успешности
|
||
function calculateSuccessMetrics(stats: UserStats): SuccessMetrics {
|
||
const taskStats = stats.taskStats
|
||
|
||
const firstAttemptSuccess = taskStats.filter(
|
||
t => t.status === 'completed' && t.totalAttempts === 1
|
||
).length
|
||
|
||
const completedTasks = taskStats.filter(t => t.status === 'completed')
|
||
const totalAttempts = completedTasks.reduce((sum, t) => sum + t.totalAttempts, 0)
|
||
|
||
return {
|
||
firstAttemptSuccessRate: (firstAttemptSuccess / taskStats.length) * 100,
|
||
averageAttemptsToSuccess: completedTasks.length > 0
|
||
? totalAttempts / completedTasks.length
|
||
: 0,
|
||
chainCompletionRate: (stats.chainStats.filter(c => c.progress === 100).length / stats.chainStats.length) * 100,
|
||
timeToFirstSuccess: 0 // Требует дополнительных данных
|
||
}
|
||
}
|
||
```
|
||
|
||
## 📈 Дашборды для фронтенда
|
||
|
||
### 1. Personal Dashboard (для студента)
|
||
|
||
```typescript
|
||
interface PersonalDashboard {
|
||
// Общий прогресс
|
||
overview: {
|
||
tasksCompleted: number
|
||
totalTasks: number
|
||
completionPercentage: number
|
||
currentStreak: number // дней подряд
|
||
}
|
||
|
||
// Текущие цепочки
|
||
activeChains: Array<{
|
||
chainId: string
|
||
name: string
|
||
progress: number
|
||
nextTask: ChallengeTask | null
|
||
estimatedTimeToComplete: number // минуты
|
||
}>
|
||
|
||
// Последние достижения
|
||
recentAchievements: Array<{
|
||
type: 'task_completed' | 'chain_completed' | 'first_try_success'
|
||
taskTitle: string
|
||
timestamp: string
|
||
}>
|
||
|
||
// Статистика по попыткам
|
||
attemptsStats: {
|
||
totalAttempts: number
|
||
successfulAttempts: number
|
||
successRate: number
|
||
}
|
||
|
||
// Рекомендации
|
||
recommendations: Array<{
|
||
type: 'retry' | 'continue' | 'new_chain'
|
||
message: string
|
||
actionLink: string
|
||
}>
|
||
}
|
||
|
||
// Генерация dashboard
|
||
async function generatePersonalDashboard(userId: string): Promise<PersonalDashboard> {
|
||
const stats = await challengeAPI.getUserStats(userId)
|
||
const chains = await challengeAPI.getChains()
|
||
|
||
return {
|
||
overview: {
|
||
tasksCompleted: stats.completedTasks,
|
||
totalTasks: stats.totalTasksAttempted,
|
||
completionPercentage: (stats.completedTasks / stats.totalTasksAttempted) * 100,
|
||
currentStreak: 0 // Требует дополнительной логики
|
||
},
|
||
activeChains: stats.chainStats
|
||
.filter(c => c.progress > 0 && c.progress < 100)
|
||
.map(c => {
|
||
const chain = chains.find(ch => ch.id === c.chainId)
|
||
const completedCount = c.completedTasks
|
||
const nextTask = chain?.tasks[completedCount] || null
|
||
|
||
return {
|
||
chainId: c.chainId,
|
||
name: c.chainName,
|
||
progress: c.progress,
|
||
nextTask,
|
||
estimatedTimeToComplete: (c.totalTasks - c.completedTasks) * 10 // 10 мин на задание
|
||
}
|
||
}),
|
||
recentAchievements: [], // Требует истории
|
||
attemptsStats: {
|
||
totalAttempts: stats.totalSubmissions,
|
||
successfulAttempts: stats.completedTasks,
|
||
successRate: (stats.completedTasks / stats.totalSubmissions) * 100
|
||
},
|
||
recommendations: generateRecommendations(stats)
|
||
}
|
||
}
|
||
|
||
function generateRecommendations(stats: UserStats): Array<{type: string, message: string, actionLink: string}> {
|
||
const recommendations = []
|
||
|
||
// Если есть задания требующие доработки
|
||
if (stats.needsRevisionTasks > 0) {
|
||
recommendations.push({
|
||
type: 'retry',
|
||
message: `У вас ${stats.needsRevisionTasks} заданий требуют доработки`,
|
||
actionLink: '/tasks?status=needs_revision'
|
||
})
|
||
}
|
||
|
||
// Если есть начатые цепочки
|
||
const inProgressChains = stats.chainStats.filter(c => c.progress > 0 && c.progress < 100)
|
||
if (inProgressChains.length > 0) {
|
||
recommendations.push({
|
||
type: 'continue',
|
||
message: `Продолжите цепочку "${inProgressChains[0].chainName}"`,
|
||
actionLink: `/chain/${inProgressChains[0].chainId}`
|
||
})
|
||
}
|
||
|
||
return recommendations
|
||
}
|
||
```
|
||
|
||
### 2. Admin Dashboard (для преподавателя)
|
||
|
||
```typescript
|
||
interface AdminDashboard {
|
||
// Системные метрики
|
||
system: {
|
||
totalUsers: number
|
||
activeUsers24h: number
|
||
totalTasks: number
|
||
totalChains: number
|
||
queueStatus: {
|
||
length: number
|
||
processing: number
|
||
avgWaitTime: number
|
||
}
|
||
}
|
||
|
||
// Метрики заданий
|
||
taskMetrics: Array<{
|
||
taskId: string
|
||
title: string
|
||
attemptsCount: number
|
||
successRate: number
|
||
avgAttempts: number
|
||
avgTimeToComplete: number
|
||
difficulty: 'easy' | 'medium' | 'hard' // на основе метрик
|
||
}>
|
||
|
||
// Активность пользователей
|
||
userActivity: {
|
||
registrationsToday: number
|
||
submissionsToday: number
|
||
peakHours: Array<{ hour: number, count: number }>
|
||
}
|
||
|
||
// Проблемные области
|
||
issues: Array<{
|
||
type: 'low_success_rate' | 'high_attempts' | 'long_queue'
|
||
severity: 'low' | 'medium' | 'high'
|
||
message: string
|
||
affectedEntity: string
|
||
}>
|
||
}
|
||
|
||
// Анализ сложности задания
|
||
function analyzeDifficulty(
|
||
successRate: number,
|
||
avgAttempts: number
|
||
): 'easy' | 'medium' | 'hard' {
|
||
if (successRate > 70 && avgAttempts < 2) return 'easy'
|
||
if (successRate > 40 && avgAttempts < 3) return 'medium'
|
||
return 'hard'
|
||
}
|
||
|
||
// Определение проблем
|
||
function detectIssues(stats: SystemStats): Array<any> {
|
||
const issues = []
|
||
|
||
// Длинная очередь
|
||
if (stats.queue.queueLength > 50) {
|
||
issues.push({
|
||
type: 'long_queue',
|
||
severity: 'high',
|
||
message: `Очередь содержит ${stats.queue.queueLength} заданий`,
|
||
affectedEntity: 'system'
|
||
})
|
||
}
|
||
|
||
// Низкий success rate системы
|
||
const systemSuccessRate = (stats.submissions.accepted / stats.submissions.total) * 100
|
||
if (systemSuccessRate < 30) {
|
||
issues.push({
|
||
type: 'low_success_rate',
|
||
severity: 'medium',
|
||
message: `Общий процент принятых заданий всего ${systemSuccessRate.toFixed(1)}%`,
|
||
affectedEntity: 'system'
|
||
})
|
||
}
|
||
|
||
return issues
|
||
}
|
||
```
|
||
|
||
## 🎯 Визуализация метрик
|
||
|
||
### 1. Progress Chart (круговая диаграмма)
|
||
|
||
```typescript
|
||
interface ProgressChartData {
|
||
completed: number
|
||
inProgress: number
|
||
needsRevision: number
|
||
notStarted: number
|
||
}
|
||
|
||
// Компонент для отображения (концепт)
|
||
function ProgressChart({ data }: { data: ProgressChartData }) {
|
||
const total = Object.values(data).reduce((a, b) => a + b, 0)
|
||
|
||
return (
|
||
<div className="progress-chart">
|
||
<svg viewBox="0 0 100 100">
|
||
{/* Реализация круговой диаграммы */}
|
||
</svg>
|
||
<div className="legend">
|
||
<div>✅ Завершено: {data.completed}</div>
|
||
<div>🔄 В процессе: {data.inProgress}</div>
|
||
<div>❌ Доработка: {data.needsRevision}</div>
|
||
<div>⚪ Не начато: {data.notStarted}</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 2. Timeline Chart (время проверки)
|
||
|
||
```typescript
|
||
interface TimelineData {
|
||
submissions: Array<{
|
||
timestamp: string
|
||
checkTime: number
|
||
status: 'accepted' | 'needs_revision'
|
||
}>
|
||
}
|
||
|
||
// График времени проверки по времени суток
|
||
function TimelineChart({ data }: { data: TimelineData }) {
|
||
const hourlyData = new Array(24).fill(0).map((_, hour) => {
|
||
const submissions = data.submissions.filter(s =>
|
||
new Date(s.timestamp).getHours() === hour
|
||
)
|
||
|
||
return {
|
||
hour,
|
||
count: submissions.length,
|
||
avgCheckTime: submissions.length > 0
|
||
? submissions.reduce((sum, s) => sum + s.checkTime, 0) / submissions.length
|
||
: 0
|
||
}
|
||
})
|
||
|
||
return (
|
||
<div className="timeline-chart">
|
||
{/* Реализация bar chart */}
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 3. Heatmap (активность по дням)
|
||
|
||
```typescript
|
||
interface HeatmapData {
|
||
dates: Array<{
|
||
date: string // YYYY-MM-DD
|
||
submissions: number
|
||
successRate: number
|
||
}>
|
||
}
|
||
|
||
// Визуализация активности пользователя
|
||
function ActivityHeatmap({ data }: { data: HeatmapData }) {
|
||
return (
|
||
<div className="activity-heatmap">
|
||
{data.dates.map(day => (
|
||
<div
|
||
key={day.date}
|
||
className="heatmap-cell"
|
||
style={{
|
||
opacity: day.submissions / 10, // Интенсивность цвета
|
||
backgroundColor: day.successRate > 50 ? 'green' : 'red'
|
||
}}
|
||
title={`${day.date}: ${day.submissions} попыток, ${day.successRate}% успех`}
|
||
/>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
## 🔔 Real-time уведомления
|
||
|
||
### События для отслеживания
|
||
|
||
```typescript
|
||
enum ChallengeEventType {
|
||
SUBMISSION_QUEUED = 'submission_queued',
|
||
SUBMISSION_CHECKING = 'submission_checking',
|
||
SUBMISSION_COMPLETED = 'submission_completed',
|
||
TASK_COMPLETED = 'task_completed',
|
||
CHAIN_COMPLETED = 'chain_completed',
|
||
ACHIEVEMENT_UNLOCKED = 'achievement_unlocked'
|
||
}
|
||
|
||
interface ChallengeEvent {
|
||
type: ChallengeEventType
|
||
timestamp: string
|
||
userId: string
|
||
data: any
|
||
}
|
||
|
||
// Event emitter для уведомлений
|
||
class ChallengeEventEmitter {
|
||
private listeners: Map<ChallengeEventType, Array<(event: ChallengeEvent) => void>> = new Map()
|
||
|
||
on(type: ChallengeEventType, callback: (event: ChallengeEvent) => void) {
|
||
if (!this.listeners.has(type)) {
|
||
this.listeners.set(type, [])
|
||
}
|
||
this.listeners.get(type)!.push(callback)
|
||
}
|
||
|
||
emit(event: ChallengeEvent) {
|
||
const callbacks = this.listeners.get(event.type) || []
|
||
callbacks.forEach(cb => cb(event))
|
||
}
|
||
}
|
||
|
||
// Использование
|
||
const events = new ChallengeEventEmitter()
|
||
|
||
events.on(ChallengeEventType.TASK_COMPLETED, (event) => {
|
||
// Показать toast уведомление
|
||
showNotification('✅ Задание выполнено!', 'success')
|
||
|
||
// Обновить статистику
|
||
refreshStats()
|
||
|
||
// Отправить аналитику
|
||
analytics.track('task_completed', event.data)
|
||
})
|
||
```
|
||
|
||
## 📱 Адаптивная аналитика
|
||
|
||
### Мобильная версия дашборда
|
||
|
||
```typescript
|
||
interface MobileDashboard {
|
||
// Упрощенные метрики для мобильных
|
||
quickStats: {
|
||
completedToday: number
|
||
currentStreak: number
|
||
nextTask: string
|
||
}
|
||
|
||
// Минимальные графики
|
||
weekProgress: number[] // 7 последних дней
|
||
|
||
// Быстрые действия
|
||
quickActions: Array<{
|
||
label: string
|
||
action: () => void
|
||
icon: string
|
||
}>
|
||
}
|
||
```
|
||
|
||
## 🎨 UI Components для метрик
|
||
|
||
### Stat Card Component
|
||
|
||
```typescript
|
||
interface StatCardProps {
|
||
title: string
|
||
value: number | string
|
||
change?: number // % изменение
|
||
trend?: 'up' | 'down'
|
||
icon?: string
|
||
}
|
||
|
||
function StatCard({ title, value, change, trend, icon }: StatCardProps) {
|
||
return (
|
||
<div className="stat-card">
|
||
<div className="stat-header">
|
||
<span className="stat-icon">{icon}</span>
|
||
<span className="stat-title">{title}</span>
|
||
</div>
|
||
<div className="stat-value">{value}</div>
|
||
{change && (
|
||
<div className={`stat-change ${trend}`}>
|
||
{trend === 'up' ? '↑' : '↓'} {Math.abs(change)}%
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Использование
|
||
<StatCard
|
||
title="Задания завершено"
|
||
value={42}
|
||
change={15}
|
||
trend="up"
|
||
icon="✅"
|
||
/>
|
||
```
|
||
|
||
## 🔍 A/B Testing
|
||
|
||
### Метрики для тестирования
|
||
|
||
```typescript
|
||
interface ABTestMetrics {
|
||
variant: 'A' | 'B'
|
||
|
||
// Конверсионные метрики
|
||
submissionRate: number // % пользователей, отправивших хотя бы одно задание
|
||
completionRate: number // % завершенных заданий
|
||
retryRate: number // % повторных попыток
|
||
|
||
// Временные метрики
|
||
timeToFirstSubmission: number
|
||
sessionDuration: number
|
||
|
||
// Качественные метрики
|
||
satisfactionScore?: number // если есть опрос
|
||
}
|
||
|
||
// Сравнение вариантов
|
||
function compareVariants(variantA: ABTestMetrics, variantB: ABTestMetrics) {
|
||
return {
|
||
submissionRateDiff: ((variantB.submissionRate - variantA.submissionRate) / variantA.submissionRate) * 100,
|
||
completionRateDiff: ((variantB.completionRate - variantA.completionRate) / variantA.completionRate) * 100,
|
||
winner: variantB.completionRate > variantA.completionRate ? 'B' : 'A'
|
||
}
|
||
}
|
||
```
|
||
|
||
## 📊 Экспорт данных
|
||
|
||
### CSV Export
|
||
|
||
```typescript
|
||
async function exportUserProgress(userId: string): Promise<string> {
|
||
const stats = await challengeAPI.getUserStats(userId)
|
||
const submissions = await challengeAPI.getSubmissions(userId)
|
||
|
||
let csv = 'Task,Status,Attempts,Last Attempt,Feedback\n'
|
||
|
||
stats.taskStats.forEach(task => {
|
||
csv += `"${task.taskTitle}","${task.status}",${task.totalAttempts},"${task.lastAttemptAt || 'N/A'}",""\n`
|
||
})
|
||
|
||
return csv
|
||
}
|
||
|
||
// Скачивание файла
|
||
function downloadCSV(csv: string, filename: string) {
|
||
const blob = new Blob([csv], { type: 'text/csv' })
|
||
const url = URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
link.href = url
|
||
link.download = filename
|
||
link.click()
|
||
URL.revokeObjectURL(url)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## ✅ Чек-лист для фронтенд-разработчика
|
||
|
||
- [ ] Интегрировать API клиент
|
||
- [ ] Настроить Context для state management
|
||
- [ ] Реализовать polling механизм
|
||
- [ ] Добавить Personal Dashboard
|
||
- [ ] Создать визуализации прогресса
|
||
- [ ] Настроить event tracking
|
||
- [ ] Добавить offline support
|
||
- [ ] Реализовать экспорт данных
|
||
- [ ] Добавить A/B тестирование
|
||
- [ ] Настроить мониторинг ошибок
|
||
- [ ] Оптимизировать для мобильных
|
||
- [ ] Добавить accessibility features
|
||
|
||
## 📚 Полезные ресурсы
|
||
|
||
- **API документация**: `CHALLENGE_API_README.md`
|
||
- **Архитектура**: `CHALLENGE_ARCHITECTURE.md`
|
||
- **React пример**: `CHALLENGE_REACT_EXAMPLE.md`
|
||
- **Быстрый старт**: `CHALLENGE_QUICK_START.md`
|
||
|
||
Используйте эти метрики и компоненты для создания информативного и user-friendly интерфейса!
|
||
|