You ship a Chrome extension. The install count in the Web Store dashboard goes up. You have no idea why. The blog post that hit Hacker News last week, the Reddit thread someone pinned, the YouTube creator you sent a code to — they all blur into one number. Chrome doesn't pass referrers or UTM parameters through the store listing, and your extension wakes up with no memory of how it got there. This guide is the practical fix that works in Manifest V3, stays within Web Store policy, and doesn't require shipping browsing history upstream.
Why install attribution is broken by default
On a normal website you read document.referrer, query params, and a cookie set by your ad platform, and you know exactly where the visitor came from. A Chrome extension has none of that — it's one of several structural gaps in the Web Store data. Three things go wrong at the same time:
- The user clicks "Install" on the Chrome Web Store listing, not on a page you control. The CWS strips referrer and query parameters, so the install event itself carries no source signal.
- When the extension wakes up,
chrome.runtime.onInstalledtells you it was just installed — but the API gives you no URL, no UTM, no anything beyondreason: "install". - There's no cross-origin handshake between your landing page's cookies and the extension's storage. They live in different worlds.
You can't fix this from inside the extension alone. The fix is a small handoff dance between your landing page and the extension via a second tab.
The approach: pass attribution through a handoff page
The technique in four parts:
- Your landing page issues a short-lived attribution ID and stores it in a cookie along with the source (utm, referrer, campaign).
- The user clicks an "Install" CTA that opens the Chrome Web Store listing.
- After install, the extension opens an onboarding tab back on your own domain.
- That onboarding page reads the cookie, learns the extension's anonymous_id (e.g. via a small bridge), and POSTs both to your backend so you can join them.
It looks like a lot, but the entire thing is about thirty lines of code split across two pages and a service-worker handler.
Step 1 — Capture the source on your landing page
Add a small script to your landing page (and every page that has an "Install" button) that records the attribution signal exactly once per visitor and stores it in a cookie:
// landing-attribution.js
(function () {
// Don't overwrite an existing attribution — first-touch model.
if (document.cookie.includes('cx_attr=')) return;
const p = new URLSearchParams(location.search);
const attr = {
id: crypto.randomUUID(),
utm_source: p.get('utm_source') || null,
utm_medium: p.get('utm_medium') || null,
utm_campaign: p.get('utm_campaign') || null,
referrer: document.referrer || null,
landing: location.pathname,
ts: Date.now(),
};
// 30-day cookie, Lax so it survives the CWS redirect.
document.cookie =
'cx_attr=' + encodeURIComponent(JSON.stringify(attr)) +
'; Max-Age=' + 60 * 60 * 24 * 30 +
'; Path=/; SameSite=Lax; Secure';
})();Critical details: SameSite=Lax (not Strict — Strict would drop the cookie when the user comes back from the Web Store), Secure (you're on HTTPS), and first-touch — never overwrite an existing attribution. The 30-day window is enough for the average Web-Store-then-install delay (most installs happen within minutes, but stragglers come back days later).
Step 2 — Send the user to the Web Store
The install button just opens your listing. There is no way to pass the attribution ID through the Web Store listing — Chrome ignores URL parameters there. So we don't try. The cookie stays on your domain, waiting.
<a href="https://chromewebstore.google.com/detail/<your-id>"
onclick="trackOutbound('install_click')">
Install
</a>You can also use the inline Chrome Web Store install URL pattern, but the principle is the same: the click goes to Google, the cookie stays with you.
Step 3 — Open an onboarding tab after install
In the extension's service worker, listen for onInstalled with reason: "install" and open a page on your own domain — a thank-you / onboarding page. This is also a good UX touch on its own: most installed extensions just sit there silently.
// background.js (service worker)
chrome.runtime.onInstalled.addListener(async (details) => {
if (details.reason !== 'install') return;
// Get the persistent anonymous ID you already generate for analytics.
const { anonymous_id } =
await chrome.storage.local.get('anonymous_id');
const url = new URL('https://your-extension-site.com/welcome');
url.searchParams.set('uid', anonymous_id);
url.searchParams.set('v', chrome.runtime.getManifest().version);
chrome.tabs.create({ url: url.toString() });
});Two things to note. First, you're opening your own page — never a third-party tracker, and never a page that loads remote analytics. Second, the URL carries the extension's anonymous_id as a query param so the welcome page can read it without needing a cross-context bridge.
Step 4 — Join the attribution ID with the anonymous ID
On /welcome, read both pieces and POST them to your backend:
// welcome.html
<script>
(function () {
const uid = new URLSearchParams(location.search).get('uid');
if (!uid) return;
const cookie = document.cookie.split('; ')
.find(c => c.startsWith('cx_attr='));
const attr = cookie
? JSON.parse(decodeURIComponent(cookie.split('=')[1]))
: { id: null };
fetch('https://your-api.example.com/v1/attribution', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
anonymous_id: uid,
attribution: attr,
}),
keepalive: true,
}).catch(() => {});
// First-touch consumed — clear so a later install on the same machine
// gets its own attribution.
document.cookie =
'cx_attr=; Max-Age=0; Path=/; SameSite=Lax; Secure';
})();
</script>On the backend, store the row keyed by anonymous_id, deduped (a re-install on the same machine will already have the same ID). From that point on, every event your extension sends carries an identity that maps back to the source.
Edge cases
- No JavaScript / cookie blocked. About 2–3% of users. They'll show up as organic attribution. That's honest — better than guessing.
- Incognito install. The landing-page cookie is incognito-scoped, so it's present at install time. The welcome tab inherits the same context. Works fine, but the cookie dies when the user closes the incognito window.
- User installs straight from the Web Store search. No landing-page hit, no cookie. These are organic — and they should be counted as such, not silently dropped.
- Re-install after uninstall. Same anonymous ID if extension storage was preserved (rare), new ID otherwise. For attribution purposes treat them as separate installs — they made the decision twice.
- Multiple landing-page visits. First-touch wins. If you want last-touch, drop the "don't overwrite" check in step 1.
Web Store policy notes
The Chrome Web Store's User Data Policy is strict but this approach is fine because:
- You only collect the attribution signal on your own domain, before the user installs. That's regular web tracking, governed by your privacy policy.
- The extension never reads the user's browsing history or third-party cookies. It just generates an
anonymous_idand opens one URL after install. - The handoff is one-way: landing-page cookie → your server, tied to your own extension's install. No third party touches it.
Disclose this in your privacy practices and you're aligned with policy and the spirit of GDPR — you're measuring marketing, not surveilling people.
FAQ
Can I just put my UTM in the Chrome Web Store install URL?
No. The Web Store strips parameters and there's no documented way to pass state through the install button. The cookie handoff is the only path that works in 2026.
What about fingerprinting the install (IP + user agent matching)?
Some teams do this — record IP + UA on the landing page, record IP + UA on the first extension call, and fuzzy-match. It works for ~70% of installs but it's privacy-hostile (you're storing IPs longer than you should), it breaks for mobile users on changing networks, and it's a red flag for reviewers. The cookie handoff is cleaner and gets you ~95% coverage.
Does this work on Edge, Brave, and other Chromium browsers?
Yes. They all honor chrome.runtime.onInstalled the same way and they all accept cookies the same way. The Microsoft Edge Add-ons store also strips parameters from listing URLs, so the technique transfers directly.
How do I attribute installs from a YouTube creator code?
Give each creator a URL like your-site.com/?utm_source=youtube&utm_campaign=creator-handle. The landing-page script captures that the same way it captures ad-network UTMs. In your backend report you'll see installs grouped by utm_campaign.
What does Crxlytics do with this?
We run this exact pipeline as part of our SDK. The extension generates an anonymous ID, our backend exposes the POST /v1/attribution endpoint, and the dashboard shows installs broken down by source, campaign, and landing-page funnel. If you'd rather not build it yourself, that's the shortest path. Otherwise, the code above is everything you need to roll your own.