StudyEvent + UserAbility System
Overview
The StudyEvent and UserAbility system provides comprehensive tracking of user learning across all modalities (read/write/listen/speak) and integrates with external SRS systems like Anki.
Key Features
1. StudyEvent - Immutable Event Log
StudyEvents are append-only records of all user interactions with learning resources:
- Resource views: When users view sentences, conjugations, verb lemmas, or grammar notes
- Audio interactions: When audio is played or generated
- Reviews: External reviews from Anki or future internal SRS
- Multi-resource support: Single card reviews can log multiple resources
2. UserAbility - Aggregated State
UserAbilities represent the current state of user knowledge for each resource:
- Per-modality abilities: Separate tracking for read/write/listen/speak
- Overall ability: Calculated from modality-specific abilities
- SRS scheduling: Due dates, intervals, and ease factors
- Statistics: View counts, review counts, success rates
3. Modalities
All study events track one of four knowledge modalities:
read- Reading comprehension (default)write- Writing/recall without promptslisten- Listening comprehensionspeak- Speaking/pronunciation
Architecture
Data Flow
User Interaction
↓
StudyEventLogger.log() or StudyEventLogger.log_multi_resource()
↓
StudyEvent created (immutable)
↓
UserAbilityUpdater.update()
↓
UserAbility updated (current state)
Multi-Resource Cards
A single card (e.g., from Anki) may contain multiple resources:
- Infinitive: perdre
- Conjugation: ils perdraient
- Sentence: Loin d’eux l’idée qu’ils perdraient la partie.
One review creates three StudyEvents, one per resource, all sharing:
- Same
external_card_idorcard_id - Same modality
- Same SRS payload (ease, interval, etc.)
- Same
happened_attimestamp
Usage
Logging Study Events
Single Resource:
StudyEventLogger.log(
user: current_user,
resource: @sentence,
event_type: :view,
source: "immersive_web",
modality: "read"
)
Multiple Resources (e.g., from Anki card):
resources = [@infinitive, @conjugation, @sentence]
StudyEventLogger.log_multi_resource(
user: current_user,
resources: resources,
event_type: :review,
source: "anki_desktop",
modality: "read",
external_card_id: "1234567890",
ease_button: 3,
interval_days: 7,
ease_factor: 2300,
review_type: "review",
deck_name: "French Verbs"
)
Updating User Abilities
User abilities are automatically updated when logging events, or can be manually triggered:
UserAbilityUpdater.update(
user: current_user,
resource: @sentence
)
Controller Integration
Include the StudyTracking concern in controllers:
class MyResourceController < ApplicationController
include StudyTracking
def show
@resource = Resource.find(params[:id])
# Event logging happens automatically via after_action
end
private
def loggable_resource
@resource # Specify which instance variable to log
end
def view_modality
"read" # Override if needed (default is "read")
end
end
Querying Study Events
# Get all events for a user
user.study_events.recent
# Filter by resource type
user.study_events.by_resource_type("Sentence")
# Filter by modality
user.study_events.by_modality("listen")
# Filter by date range
user.study_events.since(1.week.ago)
# Combine filters
user.study_events
.by_resource_type("Conjugation")
.by_modality("write")
.since(1.month.ago)
Querying User Abilities
# Get abilities for a user
user.user_abilities
# Find ability for specific resource
ability = UserAbility.find_by(user: user, resource: sentence)
# Get due reviews
UserAbility.for_user(user).due
# Get flagged items
UserAbility.for_user(user).flagged
# Check success rate
ability.success_rate # Returns percentage
API Integration
Endpoint
POST /api/v1/study_events
Single Resource
{
"resource_type": "Sentence",
"resource_id": 123,
"event_type": "review",
"modality": "read",
"source": "anki_desktop",
"external_card_id": "1234567890",
"ease_button": 3,
"interval_days": 7,
"ease_factor": 2300,
"review_duration_ms": 4200,
"review_type": "review",
"total_reviews": 10,
"lapses": 1,
"deck_name": "French Verbs"
}
Multiple Resources
{
"resources": [
{"resource_type": "Infinitive", "resource_id": 45},
{"resource_type": "Conjugation", "resource_id": 67},
{"resource_type": "Sentence", "resource_id": 123}
],
"event_type": "review",
"modality": "read",
"source": "anki_desktop",
"external_card_id": "1234567890",
"ease_button": 3,
"interval_days": 7,
"ease_factor": 2300
}
Response
{
"success": true,
"events_created": 3,
"ids": [456, 457, 458],
"happened_at": "2025-12-04T21:00:00Z"
}
Database Schema
StudyEvents
create_table :study_events do |t|
t.references :user, null: false
t.string :resource_type, null: false
t.bigint :resource_id, null: false
t.bigint :card_id
t.string :external_card_id
t.string :source, null: false
t.string :event_type, null: false
t.string :modality, null: false, default: "read"
t.datetime :happened_at, null: false
t.jsonb :metadata, default: {}
# Anki/SRS fields
t.integer :ease_button
t.integer :interval_days
t.integer :interval_seconds
t.integer :previous_interval_days
t.integer :ease_factor
t.integer :review_duration_ms
t.string :review_type
t.integer :total_reviews
t.integer :lapses
t.string :deck_name
t.jsonb :raw_provider_data, default: {}
t.timestamps
end
UserAbilities
create_table :user_abilities do |t|
t.references :user, null: false
t.string :resource_type, null: false
t.bigint :resource_id, null: false
t.string :fluent_language
t.string :target_language
# Ability scores
t.float :ability, default: 0.0
t.float :read_ability, default: 0.0
t.float :write_ability, default: 0.0
t.float :listen_ability, default: 0.0
t.float :speak_ability, default: 0.0
# SRS state
t.datetime :study_due_at
t.integer :study_interval_days, default: 0
t.float :study_ease_factor, default: 0.0
# Statistics
t.integer :view_count, default: 0
t.integer :review_count, default: 0
t.integer :correct_count, default: 0
t.integer :incorrect_count, default: 0
# Last activity
t.string :last_result
t.datetime :last_seen_at
t.bigint :last_card_id
t.string :last_deck_name
t.string :last_card_mode
t.boolean :flagged, default: false
t.timestamps
t.index [:user_id, :resource_type, :resource_id], unique: true
end
Admin Interface
Both models have full ActiveAdmin interfaces available at:
/admin/study_events- View and manage all study events/admin/user_abilities- View and manage user abilities
Testing
Comprehensive test coverage includes:
- Model validations and associations
- Scopes and queries
- Service object functionality
- API endpoint behavior
- Controller integration
- Multi-resource event logging
Run tests:
bin/rails test test/models/study_event_test.rb
bin/rails test test/models/user_ability_test.rb
bin/rails test test/services/study_event_logger_test.rb
bin/rails test test/services/user_ability_updater_test.rb
bin/rails test test/controllers/api/v1/study_events_controller_test.rb
bin/rails test test/controllers/study_events_controller_test.rb
Future Enhancements
Phase 1: Analytics
- Daily/weekly/monthly study statistics
- Heatmap visualizations
- Streak tracking
- Study time analytics
Phase 2: Recommendations
- Spaced repetition recommendations
- Weak areas identification
- “What should I study next?” suggestions
- Study goal tracking
Phase 3: Advanced SRS
- Internal SRS implementation
- FSRS algorithm integration
- Customizable scheduling parameters
- Learning curves and predictions
Support
For questions or issues:
- Check the code documentation in the models and services
- Review the spec files for usage examples
- Contact the development team via GitHub issues