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 prompts
  • listen - Listening comprehension
  • speak - 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_id or card_id
  • Same modality
  • Same SRS payload (ease, interval, etc.)
  • Same happened_at timestamp

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

This site uses Just the Docs, a documentation theme for Jekyll.