Skip to main content

Analytics Guide

The analytics services turn raw quiz activity into readable insights: how a quiz performed, which questions were too hard, who your strongest participants are, and how a live event went. This guide explains what each report tells you, how every number is calculated, and where to get it — written for people consuming the data, not building it.


Before You Start

Authentication

Every analytics endpoint requires you to identify yourself as the quiz owner, in one of two ways:

  • Authorization: Bearer <token> header (a logged-in user token), or
  • x-api-key: <key> header, where the key has the Owner role.

You only ever see analytics for your own quizzes. Asking about someone else's quiz behaves the same as asking about a quiz that doesn't exist.

The golden rules — what counts and what doesn't

Every number in every report follows the same ground rules. Keep these in mind and the figures will always make sense:

  1. Only the latest attempt counts. If a participant retakes a quiz, their previous attempts are ignored everywhere. A participant is always represented by their most recent attempt — analytics never double-counts retakes.
  2. Most reports only count finished attempts. An attempt that was started but never submitted is excluded from score-based reports. The exceptions are the activity heatmap/calendar and event stats, which deliberately look at started attempts to measure engagement.
  3. Deleted quizzes disappear. Once a quiz is deleted, its data no longer appears in any report.
  4. All times are UTC and all durations are in milliseconds.
  5. Percentages are rounded to 2 decimal places (e.g. 72.46).

Quiz Reports

Quiz Summary

GET /api/v1/analytics/quizzes/{quiz_id}/summary

The one-glance health check for a single quiz. Use it for a dashboard card: "342 attempts, 87 people, average 74%."

FieldWhat it meansHow it's calculated
total_attemptsHow many finished attempts the quiz has receivedCount of submitted, latest attempts
unique_participantsHow many different people took the quizEach person counted once, no matter how many retakes
avg_scoreThe typical raw scoreSum of everyone's score ÷ number of attempts
avg_score_pctThe typical score as a percentageaverage score ÷ quiz total points × 100
best_score / worst_scoreThe highest and lowest scores achievedSimple maximum and minimum
avg_time_taken_msHow long people typically tookAverage of each attempt's duration
total_pointsThe maximum possible scoreTaken from the quiz itself
completion_rateShare of counted attempts that were submittedSubmitted attempts ÷ counted attempts × 100
note

Because this report only looks at finished attempts in the first place, total_attempts here means submitted attempts. To see how many people started but never finished, use the trends series (below), which tracks completion rate over time.

Quiz Performance

GET /api/v1/analytics/quizzes/{quiz_id}/performance

The deep-dive on scores: how they spread out, whether people pass, and how the quiz trends over time. This is the endpoint behind a "performance" tab with a histogram and a line chart.

Optional query parameters:

ParameterDefaultMeaning
pass_threshold50The percentage score that counts as a "pass" (0–100)
bucket_count10How many bars in the score histogram (2–50)
granularitydailyTrend resolution: hourly, daily, weekly, or monthly
from / tononeLimit the trend series to a date range

What you get back:

  • mean — the average score. Add up every score, divide by the number of attempts:

    mean=sˉ=s1+s2++snn\text{mean} = \bar{s} = \frac{s_1 + s_2 + \dots + s_n}{n}
  • median — the middle score. Line everyone up from lowest to highest; the median is the person in the middle (with an even count, the midpoint between the two middle scores). Less distorted by a few extreme scores than the mean: if the mean is well below the median, a few very low scores are dragging the average down.

  • std_deviation — how spread out the scores are (sample standard deviation):

    σ=i=1n(sisˉ)2n1\sigma = \sqrt{\frac{\sum_{i=1}^{n} (s_i - \bar{s})^2}{n - 1}}

    A small number means everyone scored close together; a large number means results varied a lot.

  • pass_rate — the percentage of attempts that scored at or above the pass threshold. Each attempt's score is first converted to a percentage:

    score pct=scoretotal points×100pass rate=attempts with score pctthresholdtotal attempts×100\text{score pct} = \frac{\text{score}}{\text{total points}} \times 100 \qquad \text{pass rate} = \frac{\text{attempts with score pct} \geq \text{threshold}}{\text{total attempts}} \times 100
  • distribution — the histogram. Every attempt's percentage score is dropped into equal-width buckets between 0 and 100, where each bucket spans 100/bucket count100 / \text{bucket count} percentage points. With the default 10 buckets you get 0–10%, 10–20%, … 90–100%, each with a count of how many attempts landed there.

  • trends — a time series. For each time bucket (hour/day/week/month) you get the attempt count, unique participants, average score, average score percentage, and completion rate for that period. Completion rate here is meaningful: it's the share of attempts started in that period that were actually submitted.

Top Quizzes

GET /api/v1/analytics/trends/top-quizzes

A leaderboard of your own quizzes. Answers "which of my quizzes gets the most traffic?" or "which one do people struggle with?"

ParameterDefaultMeaning
sort_byattemptsRank by attempts, participants, avg_score, or completion_rate
sort_orderascasc or desc (use desc for a "top 10")
limit10How many quizzes to return (1–50)

Each row carries the same headline numbers as the quiz summary (attempts, unique participants, average score and percentage, completion rate, average time). Unlike the summary, this list includes quizzes with zero attempts, and its completion rate counts unfinished attempts too — so it's the honest "how many starters actually finish" number.


Question Reports

Question Stats

GET /api/v1/analytics/questions/{question_id}/stats — one question

GET /api/v1/analytics/quizzes/{quiz_id}/questions — every question in a quiz

These reports tell you whether your questions are doing their job. Two numbers matter most:

Difficulty index

difficulty_index (0 to 1) — the fraction of people who answered correctly. Despite the name, higher means easier: 0.9 means 90% got it right (easy), 0.2 means only 20% did (hard). A healthy quiz mixes values; a question near 0 or 1 tells you almost nothing about who knows the material.

difficulty index=correct answerstotal answers given\text{difficulty index} = \frac{\text{correct answers}}{\text{total answers given}}

Only the latest answer from each finished attempt is counted, and partially-correct answers count as wrong — an answer must be fully correct to enter the numerator.

Worked example: 40 people answered the question; 28 got it right.

difficulty index=2840=0.70\text{difficulty index} = \frac{28}{40} = 0.70

70% answered correctly — a moderately easy question.

Discrimination index

discrimination_index (−1 to 1) — does this question separate strong participants from weak ones? It's the Pearson correlation between the points a participant earned on this question and their score on the whole quiz:

discrimination index=r=i=1n(xixˉ)(yiyˉ)i=1n(xixˉ)2  i=1n(yiyˉ)2\text{discrimination index} = r = \frac{\sum_{i=1}^{n} (x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum_{i=1}^{n} (x_i - \bar{x})^2} \; \sqrt{\sum_{i=1}^{n} (y_i - \bar{y})^2}}

where, for each participant ii of the nn who answered:

  • xix_i = points they earned on this question
  • yiy_i = their total score on the quiz attempt
  • xˉ\bar{x}, yˉ\bar{y} = the averages of those two values across all participants

In plain words: for every participant, compare "did they do well on this question?" against "did they do well on the quiz overall?" If the two move together — strong participants get it right, weak participants get it wrong — the correlation is positive. If they're unrelated, it's near zero. If they move opposite — your best people get it wrong — it's negative.

  • Above ~0.3 — a good question: people who did well overall tended to get it right.
  • Near 0 — the question doesn't distinguish anyone (maybe trivia, maybe ambiguous). Also returned as exactly 0 when the correlation can't be computed — e.g. everyone got the question right (no variation to correlate).
  • Negative — a red flag: your best participants got it wrong. Often means a misleading wording or a mis-keyed answer.

Worked example (intuition): on a 4-person quiz, total scores are 90, 80, 40, 30. On question A the same people earned 5, 5, 0, 0 — the two strongest got it right and the two weakest got it wrong, giving r0.98r \approx 0.98: an excellent discriminating question. On question B they earned 0, 0, 5, 5 — the weakest participants got it right, giving r0.98r \approx -0.98: review that question.

Timing and answer breakdowns

  • avg_time_ms / median_time_ms — how long people spent on the question. The average is the sum of all times ÷ number of answers; the median is the middle value when times are sorted. If the median is far below the average, a few people got stuck for a very long time.

  • distractors — for multiple choice, multiple select, and true/false questions: every answer option with how many people picked it (selection_count) and what share of attempts that represents:

    selection rate=times this option was chosentotal answers to the question×100\text{selection rate} = \frac{\text{times this option was chosen}}{\text{total answers to the question}} \times 100

    This shows whether wrong options are actually tempting anyone. An option nobody picks is dead weight. (For multiple-select questions, one attempt can pick several options, so the rates can sum past 100%.)

  • most_common_wrong — the five most frequently given wrong answers. For open-text questions this is where you spot recurring misconceptions or accepted-answer gaps.

The per-quiz version accepts sort_by (difficulty, discrimination, avg_time) and sort_order (asc/desc), so "sort by discrimination ascending" surfaces your problem questions first.


Participant Reports

Participant Report

GET /api/v1/analytics/participants/{participant_id}/report

One person's full story across all of your quizzes.

  • Headline totals — how many different quizzes they attempted, total attempts, average score, and best score.

  • percentile_rank (0–100) — how this person compares with everyone else. For each quiz they took, we compute what fraction of that quiz's participants scored strictly below them:

    percentile in one quiz=participants scoring below themparticipants in the quiz1×100\text{percentile in one quiz} = \frac{\text{participants scoring below them}}{\text{participants in the quiz} - 1} \times 100

    The report then averages that percentile across every quiz they took. A value of 85 reads as: "on average, this person scores better than 85% of participants." (In a one-person quiz the percentile is 0 — there was nobody to beat.)

  • score_progression — every submitted attempt in chronological order with score, percentage, and date. Plot it to see whether they're improving.

  • category_performance — accuracy grouped by question category: total questions answered, how many were correct, and

    correct rate=correct answers in the categoryquestions answered in the category×100\text{correct rate} = \frac{\text{correct answers in the category}}{\text{questions answered in the category}} \times 100

    This is the "strong in History, weak in Math" view.

Optional filters: quiz_ids (comma-separated list) and from/to dates, which narrow the score progression.

note

The report's avg_score_pct compares the participant's average score against the largest quiz total among their quizzes. If they took quizzes with very different point totals, treat it as approximate — the per-attempt score_pct values inside score_progression are exact.

Quiz Leaderboard

GET /api/v1/analytics/quizzes/{quiz_id}/participants

Everyone who finished a specific quiz, ranked. This is your results table or leaderboard.

Each row: participant alias, score, percentage, rank, number of attempts they made, time taken, and when they submitted. Rank is by score, highest first, and ties share the same rank — two people tied at the top are both rank 1, and the next person is rank 3.

Paginated (page, per_page, default 20 per page, max 100) and sortable by score, rank, attempts, or time.

Participant × Quiz Detail

GET /api/v1/analytics/participants/{participant_id}/quizzes/{quiz_id}/report

The drill-down when you click one row in the leaderboard: one person's result on one quiz, answer by answer.

  • Their score, percentage, rank, and percentile for that quiz (percentile 90 = beat 90% of participants on this quiz).
  • answers — every question with the answer they gave, whether it was correct, the points earned, time spent, and quiz_avg_score — the average points everyone earned on that question, so you can see where this person beat or trailed the crowd.

Event Reports

Events are scheduled runs of a quiz (a live session with registration). These reports focus on turnout as much as scores.

Event Stats

GET /api/v1/analytics/events/{event_id}/stats

The post-event wrap-up. The key funnel:

FieldWhat it meansHow it's calculated
total_registeredHow many people signed upDistinct registered participants
total_attemptedHow many actually startedCount of attempts in the event
participation_rateTurnoutstarted ÷ registered × 100
total_submittedHow many finishedAttempts with a submission
completion_rateFollow-throughfinished ÷ started × 100
participation rate=startedregistered×100completion rate=finishedstarted×100\text{participation rate} = \frac{\text{started}}{\text{registered}} \times 100 \qquad \text{completion rate} = \frac{\text{finished}}{\text{started}} \times 100

So "200 registered, 150 started (75% participation), 140 finished (93.33% completion)" reads directly off this response. You also get the average score (raw and as a percentage), a 10-bucket score distribution (same histogram idea as quiz performance), and peak_participation_time — the single moment when the largest number of participants started their attempt.

Event Comparison

GET /api/v1/analytics/events/compare?event_ids=<id1>,<id2>,...

Side-by-side numbers for 2 to 10 events (comma-separated IDs), returned in chronological order: average score and percentage, completion rate, participation rate, attempted, and submitted. Ideal for "did the afternoon session outperform the morning one?"

Event Timeline

GET /api/v1/analytics/events/{event_id}/timeline

Activity during the event, minute by minute. Time is split into buckets (bucket_minutes, default 15, anywhere from 1 minute to a day) and each bucket reports:

  • new_attempts — how many people started in that window
  • submissions — how many finished in that window

Chart both lines together to see the wave of starts at the beginning and the wave of submissions near the deadline.


Platform Dashboard

These three endpoints cover all your quizzes at once — the data behind a home dashboard.

GET /api/v1/analytics/trends/overview

Two things in one response:

  • totals — lifetime numbers across your whole account: total quizzes, total distinct participants, total attempts, average score (raw and percentage), and average completion rate. These totals include unfinished attempts, so the completion rate is the true "starters who finish" figure.
  • series — the same time series as quiz performance trends (attempts, participants, average score, completion rate per period), but aggregated over all your quizzes. Accepts granularity (default daily) and from/to.

Activity Heatmap & Calendar

GET /api/v1/analytics/trends/activity

When are people taking your quizzes?

  • heatmap — a grid of day-of-week × hour-of-day, each cell counting attempts started in that slot. day_of_week runs 0 = Sunday through 6 = Saturday, and hour is 0–23 in UTC — shift to your local timezone before displaying.
  • calendar — attempts started per calendar date, ready for a GitHub-style contribution calendar.

Both count started attempts (finished or not), because here you care about activity, not outcomes. Optional from/to narrow the window.


Reading the Numbers — Quick Reference

NumberRangeRule of thumb
difficulty_index0–1Higher = easier. Aim for a mix; investigate anything near 0 or 1
discrimination_index−1–1Above 0.3 good; near 0 uninformative; negative = review the question
percentile / percentile_rank0–100"Beats X% of participants"
completion_rate0–100%Of those who started, how many finished
participation_rate0–100%Of those who registered, how many showed up
pass_rate0–100%Share of attempts at or above the pass threshold
any *_pct0–100%Score as a share of maximum points
any *_ms≥ 0Milliseconds (divide by 1000 for seconds)

Common gotchas

  • A retake replaces the old attempt everywhere. Historical charts reflect the latest state, not what a report said last week.
  • Ranks can tie. Ranks 1, 1, 3 is normal — don't assume they're consecutive.
  • Empty ≠ error. A quiz with no submissions returns a "not found or has no submissions" error rather than a report full of zeros. Treat that case as "no data yet" in your UI.
  • Timezones. Everything is UTC. The activity heatmap in particular will look shifted unless you convert to the viewer's timezone.