Statistics and analytics
Tracked event types
The application automatically records 3 types of user events:
Type (event_type) |
Triggered when | What it measures |
|---|---|---|
unlock_language |
A player unlocks a language | Popularity / sales per language |
view_card |
A player views a card | Engagement per card |
play_audio |
A player listens to audio | Recording usage |
Structure of the statistics table
CREATE TABLE statistics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
language_id UUID REFERENCES languages(id) ON DELETE CASCADE,
card_id UUID REFERENCES cards(id) ON DELETE CASCADE,
item_id UUID REFERENCES items(id) ON DELETE CASCADE,
event_type VARCHAR(50) NOT NULL, -- 'unlock_language', 'view_card', 'play_audio'
count INTEGER DEFAULT 1,
event_date DATE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Uniqueness key: (language_id, card_id, item_id, event_type, event_date) — events are aggregated per day (upsert with count increment).
Structure of the quiz_sessions table
CREATE TABLE quiz_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
language_id UUID REFERENCES languages(id) ON DELETE CASCADE,
category_slug VARCHAR(255) NOT NULL,
category_name VARCHAR(255),
total_questions INTEGER NOT NULL,
score INTEGER,
started_at TIMESTAMPTZ DEFAULT NOW(),
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
- A
quiz_sessionsrecord is created when a player starts a quiz scoreandcompleted_atare filled in when the player completes the quiz- If
completed_atis NULL, the quiz was abandoned
Viewing statistics in the dashboard
Most analyses are accessible directly in the dashboard. For advanced analyses, you can access the PostgreSQL database directly.
Useful SQL queries
Total number of unlocks
SELECT COUNT(*) AS total_unlocks
FROM statistics
WHERE event_type = 'unlock_language';
Unlocks by language (given period)
SELECT l.name, SUM(s.count) AS unlocks
FROM statistics s
JOIN languages l ON s.language_id = l.id
WHERE s.event_type = 'unlock_language'
AND s.event_date >= '2026-01-01'
AND s.event_date <= '2026-05-31'
GROUP BY l.name
ORDER BY unlocks DESC;
Top 10 most viewed cards (all languages)
SELECT c.card_number, c.type, l.name, SUM(s.count) AS views
FROM statistics s
JOIN cards c ON s.card_id = c.id
JOIN languages l ON c.language_id = l.id
WHERE s.event_type = 'view_card'
GROUP BY c.card_number, c.type, l.name
ORDER BY views DESC
LIMIT 10;
Most listened-to audio for a language
SELECT i.original_text, i.translation, SUM(s.count) AS plays
FROM statistics s
JOIN items i ON s.item_id = i.id
JOIN cards c ON i.card_id = c.id
WHERE s.event_type = 'play_audio'
AND c.language_id = 'LANGUAGE_UUID'
GROUP BY i.original_text, i.translation
ORDER BY plays DESC
LIMIT 10;
Daily activity over 30 days
SELECT event_date, event_type, SUM(count) AS total
FROM statistics
WHERE event_date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY event_date, event_type
ORDER BY event_date DESC;
Quiz performance — completion rate
SELECT
l.name AS language,
qs.category_name,
COUNT(*) AS sessions_started,
COUNT(qs.completed_at) AS sessions_completed,
ROUND(COUNT(qs.completed_at)::numeric / COUNT(*)::numeric * 100, 1) AS completion_rate,
ROUND(AVG(qs.score::numeric / qs.total_questions::numeric * 100), 1) AS avg_score_pct
FROM quiz_sessions qs
JOIN languages l ON qs.language_id = l.id
GROUP BY l.name, qs.category_name
ORDER BY l.name, qs.category_name;
Offline statistics
Offline players accumulate events in a local queue (IndexedDB). These events are synchronised automatically when the connection is restored.
Impact: - Real-time statistics may be slightly delayed for offline users - Data is always eventually consistent (no data loss) - There is no way to force synchronisation from the admin side
Exporting statistics
Via pg_dump (full export)
# On the server or via SSH tunnel
pg_dump -h localhost -U postgres -d racines \
--table=statistics --table=quiz_sessions \
-F c -f stats_backup.dump
Via psql (CSV export)
psql -h localhost -U postgres -d racines -c \
"\COPY (SELECT * FROM statistics WHERE event_date >= '2026-01-01') \
TO '/tmp/stats_2026.csv' WITH CSV HEADER;"
Via the admin API
The GET /api/admin/statistics endpoint returns aggregated statistics as JSON:
GET /api/admin/statistics?period=30d&language_id=[UUID]
There is no native CSV export in the graphical interface. For regular exports, set up a cron script on the server side.