Files
homeschool/analytics-endpoint.js
T

165 lines
5.9 KiB
JavaScript

// Enhanced Analytics API endpoint for the Spelling Practice App
// Get analytics data with additional metrics
app.get('/getAnalytics', (req, res) => {
try {
const results = readJsonSafe(path.join(DATA_DIR, files.results), {});
// Prepare analytics data structure
const analytics = {
summary: {
totalSessions: 0,
totalWords: 0,
correctWords: 0,
overallAccuracy: 0,
averageScore: 0,
mostMissedWords: [],
activityByDate: {}
},
userStats: {},
wordStats: {}
};
// Process all results data
Object.keys(results).forEach(username => {
// Skip if user has no results
if (!Array.isArray(results[username]) || results[username].length === 0) return;
// Initialize user stats if not already present
if (!analytics.userStats[username]) {
analytics.userStats[username] = {
totalSessions: 0,
totalWords: 0,
correctWords: 0,
accuracy: 0,
averageScore: 0,
trend: [], // For progress over time
recentResults: [],
problemWords: []
};
}
// Process each session for this user
results[username].forEach(session => {
// Skip invalid sessions
if (!session || !session.answers || !Array.isArray(session.answers)) return;
// Update summary counts
analytics.summary.totalSessions++;
analytics.userStats[username].totalSessions++;
// Process session date for activity tracking
const sessionDate = new Date(session.timestamp || Date.now()).toISOString().split('T')[0];
analytics.summary.activityByDate[sessionDate] = (analytics.summary.activityByDate[sessionDate] || 0) + 1;
// Collect score for trending data
if (typeof session.score === 'number' && session.answers.length > 0) {
const accuracy = session.score / session.answers.length;
analytics.userStats[username].trend.push({
date: sessionDate,
accuracy: accuracy * 100,
score: session.score,
total: session.answers.length
});
}
// Keep only most recent 10 sessions for the trend data
if (analytics.userStats[username].trend.length > 10) {
analytics.userStats[username].trend = analytics.userStats[username].trend.slice(-10);
}
// Get the 5 most recent results for display
if (analytics.userStats[username].recentResults.length < 5) {
analytics.userStats[username].recentResults.push({
date: new Date(session.timestamp || Date.now()).toLocaleString(),
score: session.score,
total: session.answers.length,
completed: session.completed
});
}
// Process individual words in the session
session.answers.forEach(answer => {
// Skip invalid answers
if (!answer || typeof answer.word !== 'string') return;
// Update word stats
const word = answer.word.toLowerCase();
if (!analytics.wordStats[word]) {
analytics.wordStats[word] = {
attempts: 0,
correct: 0,
incorrect: 0,
accuracy: 0
};
}
analytics.wordStats[word].attempts++;
analytics.summary.totalWords++;
analytics.userStats[username].totalWords++;
if (answer.correct) {
analytics.wordStats[word].correct++;
analytics.summary.correctWords++;
analytics.userStats[username].correctWords++;
} else {
analytics.wordStats[word].incorrect++;
// Track problem words for this user
const problemWordIndex = analytics.userStats[username].problemWords.findIndex(pw => pw.word === word);
if (problemWordIndex >= 0) {
analytics.userStats[username].problemWords[problemWordIndex].count++;
} else {
analytics.userStats[username].problemWords.push({
word,
count: 1
});
}
}
// Calculate accuracy for this word
analytics.wordStats[word].accuracy =
analytics.wordStats[word].correct / analytics.wordStats[word].attempts * 100;
});
// Calculate user accuracy and average score
analytics.userStats[username].accuracy =
analytics.userStats[username].correctWords / analytics.userStats[username].totalWords * 100 || 0;
analytics.userStats[username].averageScore =
analytics.userStats[username].correctWords / analytics.userStats[username].totalSessions || 0;
});
// Sort problem words by count (descending)
analytics.userStats[username].problemWords.sort((a, b) => b.count - a.count);
// Keep only top 5 problem words
analytics.userStats[username].problemWords = analytics.userStats[username].problemWords.slice(0, 5);
});
// Calculate overall metrics
analytics.summary.overallAccuracy =
analytics.summary.correctWords / analytics.summary.totalWords * 100 || 0;
analytics.summary.averageScore =
analytics.summary.correctWords / analytics.summary.totalSessions || 0;
// Find most missed words (top 5)
const missedWords = Object.keys(analytics.wordStats)
.map(word => ({
word,
missCount: analytics.wordStats[word].incorrect
}))
.filter(item => item.missCount > 0)
.sort((a, b) => b.missCount - a.missCount)
.slice(0, 5);
analytics.summary.mostMissedWords = missedWords;
res.json(analytics);
} catch (error) {
console.error('Error generating analytics:', error);
res.status(500).send('Error generating analytics data');
}
});