Nobody has started this yet — be first.
Business impact
events_persisted_total and events_deduped_total on GET /events/stats exist specifically so someone can answer "how many events actually became new rows vs. how many were harmless retries that got deduped?" without querying Postgres directly. If deduped events get silently folded into the persisted counter, events_persisted_total runs hot (inflated by however many retries happened) while events_deduped_total stays permanently at zero — making it look like the idempotency mechanism isn't doing anything, right when someone is trying to verify it is.
Problem
Stats.record_deduped(n) is supposed to add n to the deduped-events counter used by GET /events/stats. Instead it adds n to the persisted-events counter — the separate Prometheus counter update in the same method is correct; only the in-process counter is wrong.
Current behavior
Stats.record_deduped increments self.events_persisted_total instead of self.events_deduped_total; calling record_persisted(1) then record_deduped(2) leaves events_persisted_total == 3 and events_deduped_total == 0.
Expected behavior
After a flush where 1 of 3 events is new and 2 are duplicates: events_persisted_total increases by 1 and events_deduped_total increases by 2 — never both landing on the same counter.
Steps to reproduce
cd fastapi/ingest_service && .venv/bin/python -c "from app.metrics import Stats; s = Stats(); s.record_persisted(1); s.record_deduped(2); print(s.events_persisted_total, s.events_deduped_total)"3 0 — should print 1 2.Why this matters
This is a copy-paste mistake between two nearly-identical methods (record_persisted and record_deduped differ only in which counter and which Prometheus object they touch) — exactly the kind of bug that survives code review because the two methods look almost the same at a glance and the Prometheus half of the bugged method is actually correct.
Suggested approach
Compare record_persisted and record_deduped line by line in app/metrics.py. One of them is incrementing the wrong attribute on self.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_04_dedup_counter.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b fix/dedup-counter-wrong-bucketFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/dedup-counter-wrong-bucketSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inhttp://localhost:8000localhost:5432curl localhost:8000/healthzcurl localhost:8000/readyzWork the tickets in practice_tickets/tickets/ (ordered easy → hard); each names one dedicated test in practice_tickets/tests/:
pytest practice_tickets/tests/test_ticket_01_persisted_count.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 20, clean pass/fail summary
TICKET-17 needs a real Postgres to run at all (it checks which column a value lands in, which no fake/in-memory DB backend can meaningfully verify) — set TEST_DATABASE_URL=postgresql://<you>@localhost/ingest_test before running it or the full suite; without it, that one test is skipped, not failed.
Level 1
Fix a bug
Read existing behaviour, correct it.