Why our lottery ticket QR scanner showed zero successes: a path you don't log looks like a failure
In our lottery app's weekly report, 19 people had opened the QR screen and GA4 showed zero successful scans. The scanner wasn't broken. The path that real ticket QR codes took had no logging at all. How we found the measurement gap, what four similar apps do, why we switched to the bundled scanning engine, and the numbers one week after the fix.
19 people, zero successes
Our Korean lottery results app has a screen where you scan the QR code on a ticket to see whether it won. In an early-October weekly report, 19 people had opened that screen and GA4 recorded zero successful scans.
If none of 19 people could read a ticket, the scanner would be broken. But when we scanned tickets ourselves, it read them fine. The numbers didn't match what we saw in our hands, so we suspected the logging code before the scanner.
Real ticket QR codes left the app without a trace
Rereading the code that runs after a QR code is read, we found two paths:
- If the value is a URL, hand it to the external browser. Nothing is logged.
- Otherwise, parse it in the app as
LOTTO;ROUND=…;NUMBERS=…. The attempt and success events lived only on this path.
Every real lottery ticket QR code is a URL. The LOTTO;ROUND= format was a sample we made early in development; no ticket carries it. So everyone who scanned a real ticket went down the first path to the browser, and nobody ever went down the second path, the only one that logged anything. "Zero successes" wasn't a failure. It was a gap in measurement.
What a ticket QR code looks like
Before changing anything, we checked the format on real tickets.
http://qr.dhlottery.co.kr/?v=1245q061415182834q…n000000000000…
- The 4 digits after
v=are the draw number. - Each game follows as one letter and 12 digits. The letter is
qfor automatic,sfor semi-automatic,mfor manual andnfor an empty slot; the 12 digits are six two-digit numbers. - The ticket serial number comes last.
- A
vstarting withpdis a pension lottery ticket.
Older tickets carry m.dhlottery.co.kr/?v=. Opening one today returns a 307 to qr.dhlottery.co.kr and the result page loads normally (checked October 15).
Logging first
To find where people drop off, we added an event to every branch before changing any behavior.
| Branch | Event |
|---|---|
| Lottery result URL | success + URL shape |
| Pension lottery URL | success + URL shape |
| Some other URL | failure + domain |
| Not a lottery QR | failure |
| Camera permission denied | failure |
| Left without reading anything | failure (no_read) |
The URL shape replaces every digit with 9. Ticket numbers belong to the person who bought the ticket, so we don't send them to analytics; we only keep enough to tell whether the format matches.
lotto_url:qr.dhlottery.co.kr/?v=9999q999999999999q999999999999…
What four similar apps do
We installed four other apps that check lottery results by QR code, opened their APKs, and read the QR handling.
- None of the four leaves the app after a scan. Three cut up the
vvalue inside the app and compare it with the winning numbers; one opens the official result page in an in-app WebView. - Ours was the only one that handed off to the external browser. Someone sent to the browser may never come back to the app, and the app can't tell whether they saw their result.
- Three of them use the bundled version of the ML Kit barcode scanner. We were using the unbundled one.
The catch with the unbundled scanning engine
ML Kit's barcode scanner comes in two versions. The bundled version ships the recognition model inside the APK; the unbundled version has Google Play services download the model separately.
Rereading the official documentation: the unbundled version downloads the model ahead of time only when the app is installed from the Play Store. Installed any other way, it starts downloading on the first scan, and requests made before the model arrives return no result. To the user, that's a camera pointed at a QR code with nothing happening.
We also publish this app in stores other than Google Play, so we switched to the bundled version. The APK grows by about 2.4 MB, but the first scan works no matter where the app came from. We also checked that the native library in the bundled version is 16 KB page aligned.
Other changes in the same release (1.0.76, October 15):
- The official result page opens in an in-app WebView instead of the external browser.
- The same QR code read several times in a row no longer opens the result page more than once.
- Scanner errors and result page load failures are logged with a reason.
One week after the fix
QR screen events from 1.0.76 through October 22:
| Event | Users | Count |
|---|---|---|
| Opened QR scanning | 33 | 83 |
| Success (lottery URL) | 5 | 16 |
| Left without reading | 21 | 41 |
| Camera permission denied | 3 | 9 |
| Other URL | 1 | 1 |
Successes show up for the first time. Every successful URL shape in 1.0.76 was the older address (m.dhlottery.co.kr), so older tickets are being read too.
What we still don't know
- "Left without reading" is the largest group, at 21 people. With the current events we can't tell "pointed at a ticket and it didn't read" apart from "opened the screen and left". Telling them apart needs time spent on the screen and whether the camera saw anything QR-like.
- The one "other URL" was
qr.645lotto.net, from a 1.0.76 user. It is an older lottery site address. One of the apps we compared ignores the domain and parses only thevvalue, so it reads this address too. 1.0.76 only accepted the official lottery domain, so this person was told it wasn't a lottery QR code. We added this domain to the accepted list in 1.0.78 on October 17, so current versions read itsvvalue the same way.
Takeaways
- When a success rate is zero, check whether real users ever pass through the path that logs success before looking for reasons it fails. Logging can end up concentrated in code that was only ever tested with sample data.
- A branch that hands off to another app is where logging stops. Leave at least one event before handing off.
- If an event would carry something like the numbers a person bought, log only its shape.
- The unbundled ML Kit scanner can come up empty on the first scan when the app wasn't installed from the Play Store. If you distribute in other stores, consider the bundled version.
- Even once numbers appear, if "couldn't read" and "didn't scan" are mixed together, the next release needs events that separate them.
The app in this post is our Korean lottery results and lucky store app (Google Play).