18 KiB
18 KiB
Challenge Service - Аналитика и метрики для фронтенда
Краткое руководство по ключевым метрикам и аналитике для интеграции на фронтенде.
📊 Ключевые метрики для отслеживания
1. Метрики производительности
// Метрики для мониторинга
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. Метрики пользовательского поведения
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. Метрики успешности
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 (для студента)
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 (для преподавателя)
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 (круговая диаграмма)
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 (время проверки)
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 (активность по дням)
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 уведомления
События для отслеживания
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)
})
📱 Адаптивная аналитика
Мобильная версия дашборда
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
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
Метрики для тестирования
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
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 интерфейса!