Reason one: thirty
Firebase allows thirty apps per project, and a project maps to exactly one GA4 property. Keep making apps and the project has to split; split it and the iOS and Android builds of the same product land in different properties. Seeing those two side by side is the reason the instrumentation exists.
Rather than ask for a quota increase, the measuring moved in-house. The ported Android apps never went into Firebase at all.
Reason two: months of a wrong number
In July the per-screen dwell times looked off. "LogoForm spends 78% of its time on the paywall", "Pro2Camera 69%", "Artoon 58%". Nobody looks at a paywall that long.
The cause was app code, not GA4 configuration. Screen tracking was one call in onAppear with no onDisappear. Every app shows its paywall as a .sheet, and dismissing a sheet does not re-fire the parent's onAppear. Firebase keeps the last screen_view as the current screen and books later engagement against it. Every minute spent in the editor after closing the paywall was credited to the paywall.
The fix restores the previous screen when a modal closes. Everything measured before that was overstated. One number, trusted for months.
What was built
| Piece | What |
|---|---|
| Collector | Cloudflare Worker + D1, where apps send events |
| Aggregation and reports | A Go package inside the existing SaaS |
| Clients | One iOS package, one Flutter package |
Decisions made in the design.
app_idis one string per product, not per store listing. iOS and Android must send the same value to be merged.- Country is read at the edge from the connection. The device never sends it.
- IP, advertising id, precise location and personal identifiers cannot arrive: there are no columns for them, and property keys containing those words are blocked.
- With no collector URL configured, the client switches itself off. Queueing events for an address that does not exist would overflow the queue before launch day, when the events actually matter.
- Dwell is measured as enter/exit pairs. That mistake is not repeated.
Most of the first data was not people
In the first week of collection, read at face value, the list said "apps still in review have users". Sorted by fingerprint, there were three.
| Who | Fingerprint |
|---|---|
| Play pre-launch report robot | Many install ids on one build id, country US, every locale en-US, bunched into one day, day-one retention zero, dozens of screen entries per session |
| Apple reviewer | Exactly one session and one user per app, US, three to four minutes, an app version that is not released yet |
| Me | KR, ko_KR, a real device OS version, a session or two |
LogoForm over thirty days: iOS was one session, one user, KR. Android was twelve sessions, eight users, all en-US, two build ids, 182 paywall entries. Merged, that read as "13 sessions, 165 paywall views", which is not a row that can happen.
The three are now separated. The first job of the instrumentation turned out to be not counting people but subtracting what is not.