Repository navigation
report_exercise_outcome: record the outcome when session_id is empty - #891
Merged
Merged
Conversation
|
ArchLens - No architecturally relevant changes to the existing views |
int(request.form.get("session_id")) raised ValueError on "session_id="
and the answer was lost with a 500. exercise.session_id is nullable, so
parse it with type=int, which gives None for a missing, empty or
non-numeric value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mircealungu
force-pushed
the
fix/exercise-outcome-empty-session-id
branch
from
October 25, 2026 17:12
5fda5e7 to
02552c1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
1. What happened
FMD alert, 20 Oct 2026, 14:58:59 (server time, 12:58:59 UTC):
A learner on the web (Safari on a Mac, zeeguu.org) answered exercises. The web app sent each answer to
POST /report_exercise_outcomewith an emptysession_id. The endpoint crashed with a 500 before it saved anything, so these answers were never recorded. NoExerciserow was written, and the word's schedule (the next review date) was not updated. Two answers were lost (12:58:59 and 12:59:00 UTC). The answers this learner sent after that (from 12:59:17 on) went through.2. Why it happened
Step 1: what the client sends.
web/src/api/exercises.jsbuilds the form body withqs:When
exerciseSessionIdisnull,qs.stringifywrites the key with no value:So the server receives
session_id=(an empty string), not a missing field.Step 2: the server parses it. In
zeeguu/api/endpoints/exercises.py:210:request.form.get("session_id")returns''andint('')raisesValueError. This line runs before thetry:block, soroute_wrappers.py:134sees the exception, Flask answers with a 500, anduser_word.report_exercise_outcome(...)is never called. (A request with nosession_idat all would crash the same way:int(None)raisesTypeError.)Step 3: the database never needed a session.
zeeguu/core/model/exercise.py:26:The session id only says which practice session an answer belongs to (for session duration and the teacher and activity views). The answer itself, and the scheduling that follows from it, do not need it. So the crash threw away data the DB could have stored without problems.
Why the client had no session id (also see the companion note below): the session id comes from
POST /exercise_session_start, whichuseExerciseSession/useSession(web/src/hooks/useSession.js:40-47) fire when the exercises load. Until that response arrives,getExerciseSessionId()returnsnull. In the prod logs for this learner,exercise_session_startreturned 200 at 12:58:47 UTC, 12 seconds before the failing answer, and the later answers in the same session did carry the id. I could not find the code path that left the idnullat 12:58:59, so there is no web PR. See "What I could not explain" below.3. The fix
One line in
zeeguu/api/endpoints/exercises.py:Werkzeug's
MultiDict.get(key, type=int)returns the default (None) when the conversion raisesValueError, and also when the key is missing. So:session_id=471147114711session_id=None, answer savedsession_idTypeError)None, answer savedsession_id=abcNone, answer savedNonegoes toExercise(session_id=None), which the nullable column accepts. The sametype=intpattern is already used for optional form ints inaudio_lessons.py:126(timezone_offset).A test was added:
test_outcome_without_session_id_is_still_recordedinzeeguu/api/test/test_exercise_session.py. It posts one outcome withsession_id=and one with nosession_id, then checks that both getOKand that bothExerciserows exist withsession_id IS NULL.4. Why this is safe
"4711"→4711).exercise.session_idisnullable=Truewith a FK touser_exercise_session. NULL does not violate the FK.exercise.session_id(user_exercise_session.py:79,user_stats.py:148/500/1095,session_history.py:402) all filterExercise.session_id == <some session>. A NULL row is not attached to any session, so it does not appear in session summaries or session durations. That is the only cost: the answer is recorded and scheduled, but not shown as part of a session._parse_optional_session_id).abcis now dropped silently instead of returning a 500. I chose this because losing the learner's answer is worse than losing the session link.5. How it was tested
pytest zeeguu/api/test/test_exercise_session.py zeeguu/api/test/test_user_data_ownership.py: 39 passed.ValueError: invalid literal for int() with base 10: ''.How to verify in prod (after deploy)
invalid literal for int()fromexercises.py.What I could not explain (web side)
The 14-day journal has exactly 2 of these 500s, both from this one learner, one second apart. For that learner,
exercise_session_startsucceeded at 12:58:47 UTC and later answers in the same session carried the id. In the web code,getExerciseSessionId()reads a ref that is set in the start callback (useSession.js:44) and is only cleared on unmount or alearned_languagechange. A language change remounts the page anyway, becauseAppLayout.js:111keys onlearned_language. No unmount happened (there was no secondexercise_session_startand noexercise_session_enduntil 13:00:40). So I could not find a concrete client cause, and per the agreed rule I did not change the client. The NULL count in step 2 above will show whether this happens often enough to need a client fix.🤖 Generated with Claude Code