Repository navigation
Audit round 4: failed reads vs no path, atomic retake, path load Retry, analytics streak, no roll numbers in logs - #5
Merged
Conversation
Every caller of getStagesWithProgress turned a failed read into an empty list, and an empty list means "no path yet". A network or permission error told the student to complete the assessment (practice flashcards), showed generic interview questions and beginner projects, stored a generic daily tip as the day's tip, and made Analytics say "No learning path found". FirestoreManager.readPath returns PathRead: Path, NoPath or Failed. Every caller switches on it. NoPath keeps its existing behaviour; Failed shows "Couldn't load your progress. Check your connection and try again." - in the practice screens' empty state, where pull-to-refresh retries, and on Quick Practice, which the student taps again. Projects and Analytics report it through their onError callbacks, getAllCardsNeedingReview and getQuickStats return a failure, and daily tips show today's generic tip without storing it, so the next visit asks again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eTtYuuVWNwDn1mSsKbKrW
A retake reset the stage progress in one write, then deleted and saved the stages in others. When the reset went through and the save failed, the student kept the old path with its progress wiped. FirestoreManager.replacePathForRetake puts all of it in one WriteBatch, planned by retakeWrites: reset the stage fields of the progress document, delete the old stages the new path doesn't reuse (a plain set overwrites a reused id), save the new stages. Firestore applies all of it or none of it. The reset now writes only currentStage, completedStages, stageStars and lastUpdated, so it needs no read first and leaves XP and the streak alone; resetStageProgressInCloud, which read the whole document and wrote it back, is removed. Any failure still calls onRetakeNotStarted, so the student sees "Couldn't start your retake. Please try again." with Try again. The parser kept every stage the model returned. It now keeps at most MAX_STAGES = 100, so the largest batch is 1 + 100 + 100 = 201 writes, inside Firestore's 500; the batch is checked against 500 before commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eTtYuuVWNwDn1mSsKbKrW
When the saved stages (or the check for a report) could not be read, the Learning Path screen showed a toast over an empty screen, with no way to retry except leaving and coming back. It now shows the failed state fix 2 of the last round added, worded for a failed load: "We couldn't load your learning path" / "Check your connection and try again. Your progress is safe." Retry goes back through loadStagesFromFirestore. LearningPathFragment.PathFailure carries the two wordings (LOAD, BUILD) and showPathFailed sets them. A confirmed-missing report still goes to the assessment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eTtYuuVWNwDn1mSsKbKrW
AnalyticsService took the streak, and from it the learning pace and the
completion estimate, from FirestoreManager.getProgress, which reads
users/{username}/progress/current. Nothing writes that document, so the
streak was always 0 and the pace always "slow".
It now reads the progress document Home's Study Streak card reads
(users/{username}/data/progress, kept by LearningPathFragment's saveStreak)
through UserProgressSyncService.loadProgressResult, with the same default
of 1 when there is no document yet. A failed read is reported like the
other failed reads ("Couldn't load your progress...") rather than shown as
a streak of 0. How the streak is written is unchanged; XP still comes from
where it did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eTtYuuVWNwDn1mSsKbKrW
Log.w, Log.e and Log.i survive R8 in release builds. Twelve of them - Challenge1/2/3Activity's start and resume lines and six in PreReleaseEventService - carried the participant's roll number. Each keeps its message with the roll number removed. No release-level log carried a username or email. Log.d and Log.v are untouched, and no logging is added. ReleaseLogIdentifierTest scans the app's sources for Log.w/e/i/wtf calls that interpolate or concatenate a roll number, username, email, uid or display name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eTtYuuVWNwDn1mSsKbKrW
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.
Round 4 of the audit, branched from master at e68d911 (after #4 was merged). Same rules as before: one commit per fix, a JVM unit test for each fix, explicit
git addpaths, no amend or force-push. Test names avoid" : * ? < > |.This PR leaves the security-rules PR (#3) alone. It does not touch
firestore.rules,database.rules.json,firebase-rules-tests/or the docs that PR changes.Contents: 5 fixes, one commit each, in the order you gave them. For each one I reverted just the fix's logic (or, for fix 5, put master's files back), re-ran its tests, and confirmed they fail.
1. Changes
services/FirestoreManager.kt:62,:136,:368,:985; callers:ui/practice/PracticeContentActivity.kt:105,:145,:199;PracticeFragment.kt:107;services/ProjectSuggestionService.kt:58;DailyTipService.kt:41,:49,:92;FlashcardService.kt:236;AnalyticsService.kt:111,:410getStagesWithProgress()turned a failed read into an empty list, and an empty list means "no path yet".FirestoreManager.readPath()returnsPathRead:Path,NoPathorFailed. Every caller switches on it.NoPathkeeps its existing behaviour (the assessment prompt, generic interview topics, beginner projects).Failedshows "Couldn't load your progress. Check your connection and try again."onError.getAllCardsNeedingReviewandgetQuickStatsnow return a failure.getQuickStatsnow returnsResult<LearningAnalytics>; nothing calls it.FailedPathReadTest(8)services/FirestoreManager.kt:59,:125,:376,:995;UserProgressSyncService.kt:92;StageGeneratorService.kt:35,:365,:609replacePathForRetakeputs all of it in oneWriteBatch, planned byretakeWrites, so either all of it happens or none of it does:- Reset the stage fields of the progress document.
- Delete the old stages the new path doesn't reuse. A set without merge overwrites a reused id.
- Save the new stages.
The reset now writes onlycurrentStage,completedStages,stageStarsandlastUpdated, so it needs no read first and never touches XP or the streak. That makesresetStageProgressInCloudunused, so it is removed. Any failure still shows "Couldn't start your retake. Please try again." with Try again.RetakeReplaceTest(4)ui/learningpath/LearningPathFragment.kt:143,:283,:294,:374;res/layout/fragment_learning_path.xml;res/values/strings.xmlPathFailure(LOAD,BUILD) carries the two wordings. A confirmed-missing report still goes to the assessment.PathFailureTest(3)services/AnalyticsService.kt:42,:50,:124,:416users/{u}/progress/current. Nothing writes that document.streakandbestStreakfromusers/{u}/data/progress, the document Home's Study Streak card reads andsaveStreak(PR #2) keeps. It goes throughUserProgressSyncService.loadProgressResult(), and uses Home's default of 1 when there is no document yet. A failed read gives "Couldn't load your progress…", not a streak of 0. How the streak is written is unchanged. XP still comes from where it did (see table 2).AnalyticsStreakTest(4)ui/challenge/Challenge1Activity.kt:96,:146;Challenge2Activity.kt:74,:118;Challenge3Activity.kt:76,:134;services/PreReleaseEventService.kt(6 lines)Log.i/Log.wcalls, which R8 keeps in release builds, included the participant's roll number. I checked everyLog.w,Log.eandLog.iin the app, multi-line ones included. None include a username or email, and no exception message passed to them is built from one.Log.dandLog.vare untouched, and no logging was added.ReleaseLogIdentifierTest(2)Fix 2: the batch size
{}parsed as a stage, so in principle one 8192-token reply could have produced thousands.parseStagesFromResponsenow keeps at mostMAX_STAGES = 100.On the tests
pathReadOf,retakeWrites,streakFrom,paceFor,PathFailure) and the parser.FailedPathReadTestrun the realgetTodaysTip,generateProjectSuggestions,getAllCardsNeedingReview,generateAnalyticsandgetQuickStatsagainst a FirestoreManager built without its constructor. That object has no SharedPreferences, so every read throws inside its owntryand comes back as a failedResult, the same path a network or permission error takes.PathFailureTestchecks the wording thatshowPathFaileduses, not the branch that calls it.PathFailureTestreadsstrings.xml, andReleaseLogIdentifierTestscanssrc/main/java. Both resolve paths from the module directory, which is where Gradle runs unit tests.RetakeReplaceTestis rewritten, because round 3's three-write seam it tested is gone.DailyTipFallbackTestreads.tipsfromgenerateTips()'s new return type.2. Found but not fixed
services/UserProgressSyncService.ktloadProgressFromCloud;ui/home/HomeFragment.kt:140getStagesWithProgress().loadProgressFromCloudreturns null on a failed read, and Home treats null as a new student: 0 XP, streak 1.loadProgressResult()and show the connection message onFailed.services/AnalyticsService.kt:132,:421getProgress(), which reads the never-writtenusers/{u}/progress/current.generateAnalyticsfalls back to the sum of completed stages' rewards, andgetQuickStatsto 0. Fix 4 already reads the right document.totalXPfrom the same read.AnalyticsService,FlashcardService.getAllCardsNeedingReviewlastVisitDate.services/FirestoreManager.kt:558deleteAllPersonalizedStagesdeletes from the wrong path, and after fix 2 nothing calls it.services/GeminiQuizService.ktparseQuizResponse,GeminiChallengeService.kt(Cleaned response)Log.ewrites up to 500 characters (or all) of the model's reply to release logs. That is not an identifier, but it is model text.Log.d, or log only the length.LoginActivity.kt:518,:589;QuizActivity.kt:183.GeminiQuizService.kt:204,StageContentService.kt:151, others inFirestoreManager.!!inLoginActivity.GEMINI_API_KEYfield.C:/path.Out of scope as you asked, so not started: individual contestant sign-in, a server-side username lookup, the event accounts, partly generated quizzes, and rotation state on the retake screen.
3. Verification
Run on this branch's head after
./gradlew clean. JUnit XML totals: 205 tests, 0 failures, 0 errors, 0 skipped across 37 classes. That is master's 187, plus 17 in four new classes, plus 1 more in the rewrittenRetakeReplaceTest(3 → 4):FailedPathReadTest(new)RetakeReplaceTest(rewritten)PathFailureTest(new)AnalyticsStreakTest(new)ReleaseLogIdentifierTest(new)Kotlin warnings: 20, the same as master (10 deprecation warnings, each reported for both the debug and release compiles). None are new.
compileReleaseKotlinpasses.Dependencies came from the repositories in
settings.gradle.kts, with no mirror; they were already cached.Generated by Claude Code