How to track Google Play installs from website UTM parameters
12 min readWritten by: Jonathan Reis on
A website-to-app journey can be measured by campaign without identifying the same person across every step. Pass stable UTM values through the landing page and Google Play, then record the sanitized Install Referrer result when Android opens the app.

A website link does not need to identify a person in order to be useful for app acquisition. It needs to preserve one campaign vocabulary across the handoff points that matter: the referring site, the app landing page, the store link, and the first Android launch.
That distinction matters when the website and the mobile app use separate analytics projects. Trying to join browser and app identities turns a simple acquisition question into a privacy, consent, and data-model problem. Most small product teams do not need that. They need to know whether a placement on their site sent traffic to the app landing page, whether that traffic reached the store, and whether Google Play later returned the same campaign parameters to the app.
This article describes a practical implementation for that narrower question. It uses stable UTM parameters, a landing-page CTA event, and the Google Play Install Referrer API. The result is aggregate attribution by source, campaign, and placement. It is not user-level proof that one particular web click caused one particular installation.
Start by defining the question the data can answer
The useful question is usually:
Which site placement and campaign produced store traffic and installations?
That is different from:
Did this browser visitor become this installed-device user?
The first question can be answered with campaign values. The second needs an identity design that is difficult to justify for a public marketing site and still breaks when the person changes browser, device, account, or install timing.
Use four conventional fields for the acquisition path:
| Parameter | Purpose |
|---|---|
utm_source |
The referring property or channel |
utm_medium |
The type of handoff, such as referral |
utm_campaign |
The acquisition initiative being measured |
utm_content |
The placement inside that initiative |
For a studio site linking to an app landing page, the values might be:
utm_source=your-studio-site
utm_medium=referral
utm_campaign=studio-to-app
utm_content=hero
Only utm_content should vary for each placement. A header link can use header, a product card can use catalog, and a footer link can use footer. Do not invent a new campaign name for every button. That produces a dashboard full of one-off labels and makes comparisons harder than necessary.
Build the first handoff in one place
The most common implementation mistake is writing a full destination URL in each component. It works until a campaign name changes, one locale loses a parameter, or a new navigation surface is added without tracking.
Put the URL construction behind one small helper. The helper receives the locale and the placement, chooses the localized landing path, and adds the shared campaign values.
type Placement = "hero" | "header" | "catalog" | "footer";
function getAppLandingHref(locale: string, placement: Placement): string {
const path = locale === "pt-BR" ? "/pt-BR/" : "/";
const url = new URL(path, "https://your-app-landing.example");
url.searchParams.set("utm_source", "your-studio-site");
url.searchParams.set("utm_medium", "referral");
url.searchParams.set("utm_campaign", "studio-to-app");
url.searchParams.set("utm_content", placement);
return url.toString();
}
Every link to the app landing should call this helper. The source site can still record a local event such as app_destination_clicked, but the UTMs are the contract that crosses domains and analytics projects.
Keep QA traffic visible without confusing it with acquisition
Local development is valuable for testing event payloads. Disabling analytics there usually creates a blind spot: production becomes the first place where a malformed campaign, missing property, or failed flush is discovered.
Send development events, but add a cohort property such as traffic_cohort to every event. Use qa locally and external for preview or production. Then build dashboards with two separate series instead of one combined total.
This solves two operational problems. QA stays visible as evidence that links and events were exercised. Acquisition reports can use only external when the question is commercial. The mistake is not sending QA. The mistake is silently adding QA activity to external traffic.
Preserve the campaign from the landing page to Google Play
The second handoff is where many otherwise valid implementations stop working. The institutional site passes UTMs to the landing page, but the landing page points to a clean store URL. The app then sees an organic install because Google Play never received the original parameters.
Read the landing URL once, normalize the values, and add them to every Google Play CTA before navigation. Preserve existing parameters on a store URL rather than overwriting them. That makes a campaign link usable with more than one CTA placement.
const landingUrl = new URL(window.location.href);
const storeUrl = new URL(storeLink.href);
for (const [key, fallback] of Object.entries({
utm_source: "your-app",
utm_medium: "website",
utm_campaign: "store-cta",
utm_content: storeLink.dataset.placement ?? "unknown",
})) {
if (!storeUrl.searchParams.has(key)) {
storeUrl.searchParams.set(key, landingUrl.searchParams.get(key) ?? fallback);
}
}
storeLink.href = storeUrl.toString();
Record a store CTA event before navigating. Include the landing campaign fields and the CTA position. A normal browser event may be dropped when the page unloads, so use a best-effort queue flush before same-tab navigation. Do not make navigation wait indefinitely for analytics. The user reaching the store is more important than one event arriving.
The event tells you that the landing generated intent. The URL tells Google Play which campaign to pass to Android after installation. Those are separate signals and should remain separate in reporting.
Read Install Referrer once on Android
Android can ask Google Play for the install referrer after the application is installed. The API returns a raw query string, which should not be forwarded wholesale to analytics. Parse only the campaign fields you need, validate their shape, and record the result once.
const parameters = new URLSearchParams(rawReferrer);
const attribution = {
source: normalize(parameters.get("utm_source")),
medium: normalize(parameters.get("utm_medium")),
campaign: normalize(parameters.get("utm_campaign")),
content: normalize(parameters.get("utm_content")),
};
trackEvent("install_referrer_resolved", {
install_referrer_status: attribution.source ? "attributed" : "unattributed",
...attribution,
});
Persist a local “processed” flag only after an analytics provider accepts the event. If the first attempt fails because the device is offline, retry on a later app launch. If the flag is written before delivery succeeds, the only attribution result for that installation can disappear silently.
The parsing boundary is also where to reject unexpected values. A conservative allowlist for length and characters is sufficient for campaign labels. Do not send the raw referrer, ad click IDs, account details, or arbitrary query parameters as product analytics.
Make the dashboard follow the actual boundaries
The dashboard should show a chain of related measurements, not a fake user funnel. A useful first version has three sections.
Site and landing interest
- Landing page views grouped by source and campaign.
- Store CTA clicks grouped by campaign and placement.
- QA and external traffic as separate series.
Store and installation attribution
install_referrer_resolvedgrouped by installation source.- The same event grouped by campaign and placement.
- A status breakdown for attributed, unattributed, and invalid referrers.
Early product activation
- First app initialization.
- Onboarding completion.
- First meaningful product action.
The last section measures whether the installed app is actually opened and used. It does not repair attribution. A bootstrap event can happen after an older installation, and an Install Referrer result can arrive later than the original store click. Keep the labels honest.
For example, do not divide store CTA totals by Install Referrer totals and call the result “conversion rate” without a defined attribution window and compatible denominators. A person can open the store now, install tomorrow, or install from a different device. Aggregate campaign trends are still useful, but they are not a deterministic click-to-install ledger.
Verify the whole path before publishing the dashboard
Unit tests can prove URL construction and parser behavior. They cannot prove that an actual browser link retains UTMs after navigation or that Google Play later delivers a referrer to an Android install.
Use this compact verification sequence:
- Open the institutional site locally with analytics enabled.
- Inspect a link from each placement and confirm all four UTM parameters are present.
- Follow one link and confirm the app landing receives the same values.
- Inspect the landing’s Google Play CTA and confirm it still has the same parameters.
- In a release-like Android build, install through that store link and open the app online.
- Check the sanitized
install_referrer_resolvedevent in realtime analytics. - Confirm QA traffic is labeled separately from external traffic.
The previous article on keeping web and app analytics in separate runtimes covers why browser and native delivery should use explicit runtime boundaries. That same separation makes this acquisition flow easier to inspect: the web layers report campaign interest, while the Android layer reports the store-provided attribution result.
Checklist
- Use stable
source,medium, andcampaignvalues across the full path. - Vary only
contentfor the placement that generated the handoff. - Generate landing URLs through one helper.
- Preserve landing UTMs when building every Google Play CTA URL.
- Record CTA intent before navigation, with a bounded best-effort flush.
- Parse and sanitize Install Referrer values before tracking them.
- Emit the installation attribution event once, retrying if delivery fails.
- Keep QA and external traffic as separate dashboard series.
- Treat campaign counts as aggregate attribution, not a user-level causal claim.
The implementation is deliberately modest. It does not claim to know who a visitor was across sites and devices. It keeps one campaign label intact long enough to answer the acquisition question a small app team can act on: which placement is bringing people to the store, and which campaign values are still present when Android first opens the app?
Related postWhy an Expo app needs an analytics layer before adding Amplitude18 min readWritten by: Jonathan Reis on