Uninstalls are the part of the funnel nobody wants to look at and everybody needs to. The Chrome Web Store gives you a weekly bucket and nothing else: no reason, no segment, no version split, no clue whether the user spent a month with your extension or a minute. Most teams stare at the number, guess, and ship something. This guide is the diagnostic version of that conversation — the four real uninstall patterns, how each one looks in your event stream, how to ask the users who actually churned, and which pattern to fix first based on what your data says.
Why uninstalls feel mysterious
Three structural reasons the dashboard is unhelpful:
- The Web Store reports a weekly uninstall count with no per-user, per-version, or per-day breakdown. You learn that 312 people left this week. You don't learn that 280 of them left within 24 hours of installing — which would have changed everything about how you read the number.
- The user has no incentive to tell you why. They've already decided to remove your extension. The uninstall dialog is two clicks and they're gone. Whatever reason they had stays in their head.
- The event you want —
ext.uninstalled— is fired by the dying context. If you don't usesendBeaconor the post-uninstall page pattern, you don't even see the event reliably, let alone its context. We cover the reliable-delivery pattern in the metrics tracking guide.
Diagnosis starts with separating "uninstalls" into the real patterns underneath. Lumped together they cancel out; separated they each point at a specific fix.
The four real uninstall patterns
Almost every uninstall in a Chrome extension fits one of four signatures:
- Never activated — installed, never used the extension once, uninstalled within a few days. The activation funnel collapsed at step 1.
- Error-driven — the user hit an error in their first or second session, the extension misbehaved, they left. Often a version-specific spike.
- Single-session — installed, used once, never came back, eventually uninstalled. The product solved a one-off need or the activation wasn't sticky enough to form a habit.
- Site-trigger — they were on a specific website (often a competitor, sometimes a site where your extension broke) and uninstalled right after. Long-tail in aggregate; very loud per-domain.
Two more exist (paid-tier downgrade, browser-cleanup) but for most extensions they're <5% of churn each. Optimize for the four above first.
Each pattern has a detectable signature you can compute from a normal events table (project_id, anonymous_id, event_name, properties, timestamp). The next sections show each one as SQL plus what it implies.
Detecting "never activated"
Signature: an ext.installed event with no feature-use event from the same anonymous_id before the ext.uninstalled event. The user came in and nothing happened. For most extensions this is the largest single bucket — frequently 30–50% of all uninstalls.
The activation event set is the one you defined in the DAU/MAU guide — for a translator that might be translation.run; for an ad blocker, block.matched; for a clipper, clip.saved. Use the same set for activation here.
-- Never-activated uninstalls: installed, uninstalled, no feature use in between.
WITH installs AS (
SELECT anonymous_id, MIN(timestamp) AS installed_at
FROM events
WHERE project_id = $1 AND event_name = 'ext.installed'
GROUP BY 1
),
uninstalls AS (
SELECT anonymous_id, MIN(timestamp) AS uninstalled_at
FROM events
WHERE project_id = $1 AND event_name = 'ext.uninstalled'
GROUP BY 1
),
activations AS (
SELECT DISTINCT anonymous_id
FROM events
WHERE project_id = $1
AND event_name IN ('translation.run', 'clip.saved') -- your activation set
)
SELECT
COUNT(*) AS uninstalls_total,
COUNT(*) FILTER (WHERE a.anonymous_id IS NULL) AS never_activated_uninstalls,
ROUND(100.0 * COUNT(*) FILTER (WHERE a.anonymous_id IS NULL) / COUNT(*), 1) AS never_activated_pct,
ROUND(AVG(EXTRACT(EPOCH FROM (u.uninstalled_at - i.installed_at)) / 3600), 1) AS avg_hours_to_uninstall
FROM uninstalls u
JOIN installs i USING (anonymous_id)
LEFT JOIN activations a USING (anonymous_id);Two things to read from this query:
- The percentage. If never-activated is >40% of churn, your activation gap is the problem and no amount of retention work matters until you fix it. See the CWS conversion rate post — the "install → activation drop-off" section covers what to change.
- Avg hours to uninstall. Under 48h means the user decided fast — the listing oversold or the first impression failed. 48h–7d means they intended to use it and couldn't find the entry point. >7d means they forgot — onboarding never anchored the value.
Detecting "error-driven"
Signature: an error.occurred event from the anonymous_id within their first 1–2 sessions, followed by uninstall within 7 days. The user saw something break and lost trust. If you instrument errors per the error tracking guide you already have the data.
-- Error-driven uninstalls: error within first 48h, uninstalled within 7d.
WITH installs AS (
SELECT anonymous_id, MIN(timestamp) AS installed_at
FROM events WHERE project_id = $1 AND event_name = 'ext.installed'
GROUP BY 1
),
uninstalls AS (
SELECT anonymous_id, MIN(timestamp) AS uninstalled_at
FROM events WHERE project_id = $1 AND event_name = 'ext.uninstalled'
GROUP BY 1
),
errors_early AS (
SELECT DISTINCT u.anonymous_id,
properties->>'extension_version' AS version,
properties->>'kind' AS error_kind
FROM events u
JOIN installs i USING (anonymous_id)
WHERE u.project_id = $1
AND u.event_name = 'error.occurred'
AND u.timestamp BETWEEN i.installed_at AND i.installed_at + interval '48 hours'
)
SELECT
e.version,
e.error_kind,
COUNT(DISTINCT u.anonymous_id) AS error_driven_uninstalls,
ROUND(AVG(EXTRACT(EPOCH FROM (u.uninstalled_at - i.installed_at)) / 3600), 1) AS avg_hours_to_uninstall
FROM uninstalls u
JOIN installs i USING (anonymous_id)
JOIN errors_early e USING (anonymous_id)
WHERE u.uninstalled_at <= i.installed_at + interval '7 days'
GROUP BY 1, 2
ORDER BY error_driven_uninstalls DESC;The output tells you which error kind in which version drove the most uninstalls. This is the most actionable bucket — a single fix can recover the whole row. If 60% of error-driven uninstalls come from one kind: "sw_unhandledrejection" in 4.2, you ship 4.2.1 with the fix and you can quantitatively measure how many uninstalls you prevented over the next week.
Detecting "single-session"
Signature: the user activated (a feature-use event fired), but never returned for a second day, then uninstalled. The product worked once and didn't earn a second visit. This is the most ambiguous bucket: it might be retention (they liked it but forgot), it might be a one-off use case (translator they needed once), it might be a habit-formation failure.
-- Single-session uninstalls: activated, but only used on day 0.
WITH installs AS (
SELECT anonymous_id, MIN(timestamp) AS installed_at
FROM events WHERE project_id = $1 AND event_name = 'ext.installed'
GROUP BY 1
),
uninstalls AS (
SELECT anonymous_id, MIN(timestamp) AS uninstalled_at
FROM events WHERE project_id = $1 AND event_name = 'ext.uninstalled'
GROUP BY 1
),
use_days AS (
SELECT anonymous_id,
COUNT(DISTINCT date_trunc('day', timestamp AT TIME ZONE 'UTC')) AS days_used
FROM events
WHERE project_id = $1
AND event_name IN ('translation.run', 'clip.saved') -- activation set
GROUP BY 1
)
SELECT
COUNT(*) FILTER (WHERE ud.days_used = 1) AS single_session_uninstalls,
COUNT(*) FILTER (WHERE ud.days_used >= 2) AS multi_session_uninstalls,
ROUND(AVG(EXTRACT(EPOCH FROM (u.uninstalled_at - i.installed_at)) / 86400), 1)
FILTER (WHERE ud.days_used = 1) AS avg_days_to_uninstall_single
FROM uninstalls u
JOIN installs i USING (anonymous_id)
JOIN use_days ud USING (anonymous_id);If single-session is the dominant bucket, the highest-leverage move is the second-session hook: a notification anchor, a feature that benefits from repeated use, a clear "come back when X" signal. Re-engagement work is wasted before you've diagnosed this — without the diagnostic you might be retaining users who would have stayed anyway and missing the ones who actually need the nudge.
Detecting "site-trigger"
Signature: the user's last page.viewed event before ext.uninstalled is on a specific domain, and that domain appears disproportionately across uninstalls. Two common sub-patterns:
- Competitor visit. User lands on a competing tool's landing page, decides to switch, uninstalls your extension on the way.
- Break-on-site. Your extension misbehaves on a particular high-traffic site (your CSS overrides theirs, your content script throws on their page, you block a script they need). Users uninstall because of the break rather than disliking the extension overall.
Critical privacy note: store only domain(origin), not full URL. The full path of a page is browsing history and should never leave the user's machine. We cover the boundary in detail in privacy-first extension analytics.
-- Top last-domains before uninstall, vs base-rate of those domains.
-- If a domain appears 8% of the time across all activity but 35% of the
-- time as the last-domain before uninstall, that's a strong site-trigger.
WITH last_domain AS (
SELECT DISTINCT ON (e.anonymous_id)
e.anonymous_id,
e.properties->>'domain' AS domain
FROM events e
JOIN events u
ON u.project_id = $1
AND u.anonymous_id = e.anonymous_id
AND u.event_name = 'ext.uninstalled'
WHERE e.project_id = $1
AND e.event_name = 'page.viewed'
AND e.timestamp < u.timestamp
ORDER BY e.anonymous_id, e.timestamp DESC
),
base_rate AS (
SELECT properties->>'domain' AS domain,
COUNT(*)::float / SUM(COUNT(*)) OVER () AS base_share
FROM events
WHERE project_id = $1 AND event_name = 'page.viewed'
GROUP BY 1
)
SELECT
ld.domain,
COUNT(*) AS uninstalls_with_this_last,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS share_of_uninstalls_pct,
ROUND(100.0 * br.base_share, 1) AS share_of_normal_activity_pct,
ROUND(COUNT(*) / NULLIF(SUM(COUNT(*)) OVER () * br.base_share, 0), 2) AS lift
FROM last_domain ld
JOIN base_rate br USING (domain)
GROUP BY 1, br.base_share
HAVING COUNT(*) >= 5 -- denoise
ORDER BY lift DESC NULLS LAST
LIMIT 20;Lift > 3 is interesting; lift > 6 is loud. Check the top domains by hand — if they're your own help page or your landing page, that's users who decided to read the docs and uninstall instead of contacting you (fix the docs). If they're a competitor, your CVR work + first-impression matter more than feature parity. If they're a major site like Gmail or YouTube, you almost certainly have a break-on-site bug.
Asking the user directly: uninstall surveys
The four detectors above tell you the shape of churn. The user's own words tell you the cause. The two are complementary, not redundant.
Chrome lets you set a post-uninstall URL via chrome.runtime.setUninstallURL(). Point it at a small page that asks a single question with 4–5 preset options plus a free-text field. Three rules that make this honest:
- One question, 30 seconds max. The user already left. Anything beyond a single screen drops response rate below 1%.
- Send the response back as a normal event (
uninstall.feedback_submittedwith properties{ reason, feedback }) so it goes through your existing dedup and schema. Same pipeline as everything else, one fewer ingestion path to maintain. - Brand the survey clearly. A "powered by Crxlytics" (or your own equivalent) footer keeps trust intact — the user knows where their answer is going.
Expected response rates: 5–15% is typical, 20%+ is rare and usually means a passionate user base. Don't expect 50%. The signal isn't the volume — it's the distribution across reasons combined with your behavioral detectors above. If "too complicated" dominates the survey AND never-activated dominates your detector, you have a converging diagnosis: the first 60 seconds need work.
Combining signals into a root-cause view
Each uninstall can be tagged with multiple patterns — they aren't mutually exclusive. A user can be both error-driven and single-session. The right dashboard view ranks uninstalls × patterns as a stacked bar over time, version, and acquisition source. Three concrete comparisons that change decisions:
- By version. Did pattern X spike with version 4.2? Most likely a regression specific to that release.
- By source. Do users who came from your Reddit post churn differently than users who came from organic CWS? Source × pattern is one of the highest-signal joins. Pair with install attribution.
- By time-to-uninstall bucket. <1h, 1–24h, 1–7d, 7–30d, >30d. The buckets line up with patterns: <1h is almost always never-activated; 1–7d is the error-driven sweet spot; >30d is mostly site-trigger or retention decay.
What to fix first, by dominant pattern
The fix is different per pattern. Don't treat "reduce churn" as one problem:
- Never-activated dominates → fix onboarding and the first-60-seconds path. Open the welcome tab from
chrome.runtime.onInstalled, put the first useful action in the first interaction, default the configuration for the 80% case. - Error-driven dominates → the affected version + error kind from the SQL is your bug list. Ship a patch, then re-measure: error-driven uninstalls should drop within 2 weeks of the patched version reaching adoption.
- Single-session dominates → second-session hook. The clearest win is a daily-utility wrapper: notify when the extension would help, anchor it to a workflow, give a reason to come back.
- Site-trigger dominates → look at the top domains. If competitor: improve the listing CVR + first impression. If break-on-site: file your own bug, hot-patch. The fix is usually faster than the other three buckets — and measurable because the domain stops appearing in the lift chart.
If you're tempted to do all four at once, don't. Each fix has a different signature in your data. Sequencing them lets you attribute which work moved the number.
FAQ
How soon after install do most uninstalls happen?
For most extensions, 40–60% of all uninstalls happen within 48 hours of install — almost entirely never-activated and a slice of error-driven. The long-tail churn after day 30 is usually 10–20%. If your distribution is flatter than that, your onboarding is unusually strong (or your detector for never-activated is too generous).
Do 1-star reviews predict uninstall spikes?
Sometimes — but usually the review is downstream of an error spike. If you have error tracking with version split per the error guide, you'll see the error spike 1–3 days before the review and the review 2–5 days before the uninstall wave. Watch the first signal, not the third.
Can I re-target uninstalled users?
Not directly. Chrome doesn't expose any way to contact a user after install. What you can do: capture their email at the survey step only if they volunteer it, and re-engage with a fix announcement when the bug that drove their pattern is shipped. Never opt them in silently — it kills trust and violates the Web Store spirit.
What's a "normal" uninstall rate?
For range-bound metrics (uninstalls ÷ installs in the same period): 30–60% is typical at the 7-day mark, 50–75% at 30 days for a new extension still finding product-market fit. For a mature extension with a known audience, those numbers drop to 15–30% and 30–50% respectively. Comparing to the wider funnel is in the CWS conversion rate guide.
Can I ask survey questions to users who already uninstalled?
Only via the post-uninstall page set by chrome.runtime.setUninstallURL(). Once the user leaves that page, you have no further channel. Make the single question count.
Should I show different surveys to never-activated vs long-term users?
Worth it once you've cleared the basic version. Pass the anonymous_id's feature-use count through to the survey page (via a query param signed server-side) and branch the question: never-activated users get "what stopped you from trying it?"; long-term users get "what changed?". The answers diverge sharply and so do the fixes.
Does the SDK side affect what I see?
Massively. If ext.uninstalled isn't firing reliably (no sendBeacon, no post-uninstall page, no dedup), your detectors are looking at a biased sample. Check the metrics tracking guide for delivery patterns; we also discuss the gap between SDK and CWS-reported uninstalls in why CWS analytics are inaccurate.