Our users moved to Sweden overnight
Analytics said a big share of Paperspell's users suddenly lived in small Swedish towns. They weren't bots and they weren't Swedish: a floating SDK version had started sending events from 127.0.0.1.
A colleague asked why our app, Paperspell, suddenly had bots from a small town in Sweden. They weren’t bots. They weren’t Swedish. And we never wrote the code that put them there.
What we saw
In PostHog, a big share of our daily users showed up in Sweden. First Kristinehamn, then Säffle, then Lidköping. Real people from Africa, Peru and Turkey suddenly “lived” there. Some looked like brand-new users who did nothing except appear.
The obvious answer was wrong
“Bots” was the first guess. The cheap check said no: if bots were flooding in, sign-ups would jump. They didn’t.
What it really was
These events weren’t sent by our app. They came from PostHog itself: an internal library, IP address 127.0.0.1, each one quietly removing a push-notification subscription from a user’s profile.
Two things turned that into “Sweden”:
- the GeoIP step placed 127.0.0.1 in Sweden, in a different town after each database update;
- the event overwrote the user’s location. A real user in Peru became Swedish. After a logout, a fresh anonymous profile appeared with nothing but that one event: a phantom new user.
Why it started
We never turned on push notifications through PostHog. Our Flutter SDK pulled in the native Android SDK through a floating version range. A newer minor version began registering every device’s push token with PostHog by default, as long as Firebase Messaging was in the app. Ours had it, for our own notifications. A dependency changed what our app did without a single commit on our side.
The trap in the fix
Turning the new flag off wasn’t enough. It only stops new registrations. A device that had already registered keeps the record and re-registers it on every logout, flag or no flag. It took one explicit unregisterPushNotificationToken() call after setup to stop it, plus a PostHog transformation that drops exactly these internal events for app versions still out there.
What I took from it
- A floating version range means someone else decides when your app’s behaviour changes. Pin it, and read the changelogs of what you pull in transitively.
- A feature an SDK enables by default is a feature you shipped.
- Before trusting a map, check where the IP came from. 127.0.0.1 is not a user.
- When analytics surprises you, check a second source that can’t be fooled the same way.
What’s the strangest thing your analytics told you that turned out to be your own tooling?
