Tracer Proxy

Download on the App Store

How to see what data apps are sending about you

By Ryan · Published 2026-07-28

You can read your own apps' outgoing requests on a stock iPhone, and the surprise is rarely the app's own API — it's everything riding along beside it. Record a session while you use one app, then group the requests by who receives them. The example in this guide is a single POST captured on 2026-07-24 from an app on my phone: 254 milliseconds, a 14,640-byte body, and eight analytics events, five of which were the app reporting which A/B tests I had been sorted into.


Start with the free thing iOS already gives you

iOS has a built-in list of every domain your apps contact, at Settings → Privacy & Security → App Privacy Report. Turn it on and it records, for the last seven days, which domains each app reached and which sensors each app touched. It costs nothing and it's the right first stop.

What it will not show you is a single byte of what was said. No request bodies, no headers, no payloads — just hostnames and counts. Everything interesting in this guide, including the payload below, is invisible to it. To see whether the beacon going to a vendor carries your email address or your A/B test assignments, you have to decrypt the request.

Capture one app, for two minutes, on purpose

A useful capture is short, deliberate, and covers one app. Start a session, open the app you're curious about, do the one thing you want to understand — cold launch, sign-in, a search, opening a paywall — then stop and read it. Two minutes of one app is something you can go through; twenty minutes of general phone use is a pile.

The capture is device-wide, not per-app, so anything refreshing in the background lands in the same session — force-quit the other apps first. The mechanics, certificate and all, are in the guide to inspecting HTTPS traffic without a Mac, and setup takes about a minute.

Most of the destinations aren't the app's servers

In a capture of one indie iOS app on 2026-07-23, five separate organizations appeared behind a single session: PostHog and Google for analytics, Axiom for logging, RevenueCat for subscription state, and Supabase for the actual backend. One of those five is the app. The other four are vendors the developer wired in, each receiving its own stream of events, each with its own retention policy and its own idea of what to do with the data.

Geography comes along for free, because every connection has a remote IP address. The request below left a phone in Los Angeles and terminated in AWS us-east-1, in northern Virginia — which is where the data sat, whatever the app's privacy policy said about the company's home country.

Tracer request detail over a 3D globe: an orange arc from Los Angeles to northern Virginia, and a sheet showing POST us.i.posthog.com /batch/, 200 OK, 254 ms, posthog-react-native, AWS us-east-1.
One analytics upload, captured 2026-07-24 at 10:53 AM. The arc ends at the remote IP's datacenter, not at the company's headquarters: AWS · us-east-1 is northern Virginia. The 10 next to Request Headers is the count for this exchange.

What one analytics beacon actually contained

The exchange above is a POST to us.i.posthog.com/batch/, sent by posthog-react-native/4.45.5 from an iPhone 13 Pro running iOS 26.5.2, answered 200 OK in 254 ms with a 15-byte body reading {"status":"Ok"}. The request body was 14,640 bytes and held eight events. The first event alone carried 41 properties:

Notably absent: no email address, no phone number, no location, no contacts, and "$is_identified" : false — the SDK's own marker that it had not been handed a real user identity. This payload is pseudonymous. That is what an ordinary analytics beacon looks like, and it's worth knowing the shape of the ordinary case before deciding anything is wrong.

Five of the eight events were the app telling a vendor which tests I'm in

Five of the eight events in that batch were $feature_flag_called — the PostHog SDK reporting that the app had checked a feature flag. Not a tap, not a screen view, not anything I did. The app asking itself a question, and then filing a report about having asked.

Every event in the batch also carried the whole flag map as $feature/ properties. Thirteen of them, by internal name:

"$feature/featureAppToWeb"                        : false
"$feature/featureDynamicPaywall"                  : "default"
"$feature/survey-targeting-7a03fdcb09-custom"     : true
"$feature/survey-targeting-ac0c78ea07-custom"     : true
"$feature/testAAOnboarding"                       : false
"$feature/testAAOne"                              : false
"$feature/testAAThree"                            : false
"$feature/testAATwo"                              : false
"$feature/testAiChatAgent"                        : false
"$feature/testChokingQuiz"                        : false
"$feature/testDefaultPaywallDP"                   : "in_house"
"$feature/testGBAnnualPricing"                    : false
"$feature/testOnboarding"                         : "onboarding_v3"
Tracer showing the decrypted request body of a PostHog batch upload: $feature_flag properties, thirteen $feature/ keys including testAiChatAgent and testDefaultPaywallDP, then $is_identified false and $lib posthog-react-native.
The decrypted body of the same request. Reading down the $feature/ keys gives you the app's experiment roster — including the annual-pricing test and an AI chat agent that has no visible entry point in the shipped UI.

The flag metadata is more specific than the flag values. This event recorded "$feature_flag" : "featureDynamicPaywall", "$feature_flag_id" : 714773, "$feature_flag_version" : 11, and "$feature_flag_reason" : "Matched condition set 1" — the server's explanation of why this phone was bucketed the way it was.

None of this is the app misbehaving, and it isn't about me. Sending the full flag map on every event is how the PostHog SDK is built to work, so that any event can be sliced by experiment later. The size and contents of that payload were decided by a vendor's default, not by anything the developer wrote or anything I tapped. That's the general rule for reading these: the payload usually tells you more about the SDK than about the app, and much more about the SDK than about you.

You can spot an SDK that never sends a request of its own

Some SDKs identify themselves in headers on other vendors' requests, so an inventory built only from hostnames will miss them. The PostHog request above carried two headers that have nothing to do with PostHog:

baggage:      sentry-environment=production,sentry-release=5.7.0,
              sentry-public_key=…,sentry-trace_id=…,sentry-org_id=…
sentry-trace: …

That is Sentry's distributed tracing, which instruments the app's HTTP client and stamps outgoing requests regardless of where they're headed. It reveals the crash reporter in use, the app's release string — 5.7.0 — and the environment name, on a request to an unrelated company. Read the headers of every request, not just the bodies, and not just the hostnames.

Which domains belong to whom

Analytics, attribution, and crash-reporting SDKs use a small number of well-known ingest hosts. These are the ones worth recognizing on sight; the User-Agent on the request usually confirms the guess.

Treat that as a starting point rather than an authority. Vendors add and retire hostnames, several offer customer-specific subdomains precisely so their traffic doesn't look like theirs, and a company that proxies events through its own domain won't appear here at all. The User-Agent is the more durable signal — posthog-react-native/4.45.5 names both the SDK and its version, and it's what Tracer reads to label each request with the organization behind it, alongside a bundled list of 424 organizations and 1,091 host patterns.

What's ordinary, and what's worth a second look

Most of what apps send is dull, and knowing which parts are dull is what lets you notice the parts that aren't. Ordinary, in the sense of present in nearly every app's traffic:

Worth stopping on, and worth writing down when you see it:

What you can do once you've seen it

The controls that exist are narrower than the problem, and it's better to know which is which than to assume the switches do more than they do.

What a capture can't tell you

A capture proves what left your phone, on one device, in one session, for one app version. It says nothing about what happens on the far end, how long the data is kept, who it's sold to, or why the developer chose to send it — and a payload is evidence of behavior, never of intent.

The technical gaps are just as important:

FAQ

Can I see what data an app is sending without installing anything?

Partly. iOS has a built-in App Privacy Report at Settings → Privacy & Security → App Privacy Report, which lists every domain each app contacted in the last seven days, plus the sensors and data each app accessed. It shows you who an app talked to but never what it said — there are no request bodies, headers, or payload contents in it. For the domain question it is free, requires no certificate, and is the right first stop.

Does turning off Ask App Not to Track stop apps from sending data?

No. Denying App Tracking Transparency makes ASIdentifierManager.advertisingIdentifier return the all-zero UUID 00000000-0000-0000-0000-000000000000, so an app can no longer read the cross-app advertising identifier. Analytics keeps working: SDKs generate their own first-party identifier per install and keep sending device model, OS version, locale, screen size, app version, and event history to their own servers. ATT restricts one identifier, not the beacon.

Does an app sending my device model and screen size mean it is tracking me?

Not by itself. Device model, OS version, locale, timezone, and screen dimensions are attributes shared by millions of phones, and analytics SDKs attach them to every event by default. What makes a payload identifying is the stable identifier next to those attributes — a distinct_id, device_id, or advertising identifier that persists across sessions and links each event to the last. Look for the identifier, then ask how widely it is shared.

How do I tell which company a domain in my traffic belongs to?

Read the User-Agent header first — analytics SDKs usually name themselves in it, for example posthog-react-native/4.45.5 or FBiOSSDK. Vendor hostnames change over time; a self-identifying SDK string is more stable and more specific than a hostname lookup. Failing that, the vendor's own documentation lists its ingest hosts, and the App Store privacy label names the categories of data the developer says it collects.

Is it legal to inspect the traffic my own phone sends?

This is not legal advice, and the answer depends on where you are. Recording the traffic your own device sends over your own network is ordinary self-inspection, and the same technique is standard practice in mobile development and security research. Separate questions do apply: many apps' terms of service prohibit reverse engineering, captured payloads routinely contain live API keys and session tokens that should not be published, and republishing another person's data is a different matter entirely from reading your own.


Pick one app you use every day, capture two minutes of it, and read every request once. You'll learn more from that than from any privacy policy, and the second app takes ten minutes instead of an hour because the same six vendors keep showing up. If all you want is the list of domains, iOS gives it to you free in the App Privacy Report. To read what's actually inside the requests — the identifiers, the flag maps, the vendor headers riding on someone else's call — Tracer does it on the phone, and names the company behind every host it captures. We publish our own captures in the teardowns, field by field.