Stats and answer times¶
The Stats report shows how long the senior takes to answer quiz questions. It opens from the Settings menu (the gear in the top right of the home screen, then Reports > Stats). There is no role gate: anyone using the app can view it from that link.
What is recorded¶
Every answered question in Stories from the Past, the Music Quiz, and the Faces check saves one row. The response time is exactly question end time - question start time, in milliseconds. No fancier math is applied when recording.
The video quiz is deliberately not timed and writes nothing to the stats database. It keeps its own research logging (src/features/video-quiz/logging.ts), which is unchanged.
Each row is a QuizStat (src/features/stats/types.ts) and every value of the response is kept:
| Field | Meaning |
|---|---|
quiz |
stories, music, or faces |
format |
Question type: multiple-choice, this-or-that, open-recall, reminiscence, artist-choice, or name-recall |
questionId, prompt |
Which question, and its wording at the time |
responseId, responseLabel |
The chosen answer (null for a reminiscence "Continue") |
correct |
true, false, or null when the prompt is ungraded |
responseTimeMs |
Question end time minus question start time |
inputMethod |
tap or voice |
answeredAt |
ISO 8601 timestamp |
Where it is stored¶
The stats database is on the device: an append-only JSON list under the AsyncStorage key quiz-stats-v1 (src/features/stats/storage.ts). Recording is best-effort, so a storage failure can never interrupt a session. Malformed rows are dropped one at a time on read; one bad row never wipes the history.
The quizzes currently run without an account on the phone, which is why the device holds the truth. When mobile sign-in against the Workers API lands, these rows should sync to a quiz_stats table in D1, scoped to the memory bank like every other stateful record. The row shape above was chosen so it can become that table's columns without a rework.
The report¶
Categories are one question type within one quiz (for example "Stories ยท Multiple choice"). For each category the report shows the average response time and how many answers it is built from, plus the individual saved answers, newest first. Times are shown as raw milliseconds for now, for example 4,231 ms.
Test prototypes: previewing a full month¶
A real month of answers takes a month to collect, so the report carries prototype buttons under Test prototypes:
- Preview a full month fills the report with a generated month of sample answers (
src/features/stats/sample-data.ts): every category most days, times jittered around each category's typical speed, about four in five answers right. - Preview a month with slow days is the same month with a couple of days running about 2.5 times slower, for prototyping the abnormal-day view planned below.
- Show my real data returns to the saved rows.
Previews are held in memory only. They are never written to the stats database, so sample rows can never pollute the real history.
Plan: easier-to-understand time labels¶
Raw milliseconds are the first cut. The follow-up replaces them with a plain-language label, keeping the exact number as secondary detail:
| Response time | Label |
|---|---|
| under 2 seconds | "Right away" |
| 2 to 5 seconds | "A few seconds" |
| 5 to 15 seconds | "Took some time" |
| over 15 seconds | "Took a while" |
Steps, all inside src/features/stats/:
- Add
friendlyDuration(ms)next toformatMsinaggregate.ts, returning the label plus a spoken-friendly form ("about 4 seconds") for accessibility labels. - Swap the report's primary text to the label; show
formatMsunderneath as a caption so nothing is lost. - Tune the thresholds per category once real data exists (a "this or that" answer is naturally faster than open recall), keeping the table above as the default.
Plan: spotting an abnormally fast or slow day¶
Goal: the report should be able to mark a day when answers were clearly faster or slower than that person's own normal. The raw rows saved today are enough input; nothing new needs to be recorded.
- Group rows by local calendar day (from
answeredAt) and category, and take the day's median response time. The median resists one distracted answer better than the mean. - Build the baseline from the previous 14 days that have answers (not calendar days, so a skipped week does not thin the data). Compute the baseline median and the median absolute deviation (MAD).
- Score the day with a robust z-score:
z = 0.6745 * (dayMedian - baselineMedian) / MAD. Flagz <= -2.5as "faster than usual" andz >= 2.5as "slower than usual". - Guards before flagging: at least 4 answers that day and at least 5 baseline days, and skip the comparison when MAD is 0 (identical baseline values). Otherwise say nothing rather than guess.
- Surface it as a gentle, plain-language note on that day in the report ("Answers on Jul 28 were slower than usual"), never as a diagnosis. Wording stays warm, consistent with the app's no-pass/no-fail voice.
- Implement as pure functions in
src/features/stats/analysis.tswith table-driven tests; the screen only renders the result. When stats later sync to D1, the same analysis can run in the web app for Loved Ones.