- Safari
- Analytics
- Getting started
Safari extension analytics: how to measure installs, enabled users and churn for a Safari web extension
Safari extension analytics explained: what App Store Connect counts, how to check if your Safari web extension is enabled, and how to measure users and churn.
- Author
- Extenify Team
- Published
- Reading time
- 12 min read

On this page
Safari extension analytics is harder than it looks. On the Chrome Web Store, every listing shows a user count, and the developer dashboard adds installs and uninstalls. A Safari web extension has neither. It ships inside an app, the App Store counts downloads of that app, and the person who downloaded it still has to switch the extension on in Safari settings and grant it access to websites before it does anything. Each of those steps can lose users, and none of them shows up in a store dashboard as "extension users".
This guide explains what Apple's numbers actually measure for a Safari extension, how to close the gap with two small pieces of code (one in the containing app, one in the extension), and how to line Safari up next to Chrome, Edge and Firefox without comparing different things. Every API and metric here was checked against Apple's documentation and MDN's compatibility data on October 13, 2026.
Key takeaways
- App Store Connect measures the app that contains your extension: impressions, product page views, downloads and, for users who opt in, sessions, active devices and deletions. It has no separate "extension users" metric.
- Downloaded is not enabled. Users must turn the extension on in Safari and grant website access. Measure that step yourself.
- The containing app can read the enabled state with
SFSafariExtensionManager: on macOS since 10.12, and on iOS, iPadOS and visionOS since the 26.2 updates.runtime.setUninstallURLexists in Safari but, according to MDN, "has no effect". Measure churn from heartbeats that stop arriving, plus App Store deletions.- Any telemetry you add must match your App Privacy details and Apple's data use rules, so keep it anonymous and minimal.
Why Safari extension analytics is different
Three things separate Safari from the other extension stores.
The extension lives inside an app. Apple's Safari extensions page describes the release path: you upload the extension to App Store Connect for distribution on the App Store. Apple's WWDC26 session Create web extensions for Safari builds one from scratch, "no Xcode required", lets the Safari Web Extension Packager wrap it in an app, and notes the result "will work in Safari on iOS, iPadOS, macOS, and visionOS simultaneously." The store, its ratings and its download counts all belong to the app record.
Installing is not enabling. After the download, the user opens Safari settings and turns the extension on. Until then, the extension is installed and inert. Many users download an app, never open it, and never find the toggle.
Users grant website access one site at a time. In the same WWDC26 session, Apple says: "we put the user in control and they decide which sites the extension can access." An enabled extension that was never granted access to the site it is built for will look installed and enabled in every number you have, and still do nothing.
So a Safari extension has a longer funnel than a Chrome extension, and the store only measures its first half.
What App Store Connect tells you about a Safari extension
App Store Connect's analytics are the official numbers, and they are useful. Here is what they count, from Apple's metric definitions:
| Metric | Apple's definition, in short | What it means for an extension |
|---|---|---|
| Impressions | Times the app was viewed on the App Store for more than one second | Reach of your listing |
| Product Page Views | Times the product page was viewed | Interest in the listing |
| First Time Downloads | First downloads on a device | New installs of the app, not of the extension |
| Sessions | Times the app was used for at least two seconds | Use of the containing app, which most extension users rarely open |
| Active Devices | Devices with at least one session in the period | Again, the app, not the extension |
| Deletions | Times the app was deleted | The closest thing to an uninstall count |
Two caveats change how you read these. First, the usage metrics only cover people who agreed to share data: Apple's app usage page says "App usage data is collected only from users who have agreed to share diagnostics and usage information with app developers." Second, neither page mentions app extensions at all. Apple does not document whether time spent using your extension in Safari counts toward the containing app's sessions, so do not treat Sessions or Active Devices as extension usage.
The store-side numbers are still worth watching every day. Your rating, review count and version live on the App Store listing, and our guide to the iTunes Search API shows how to read them for any app programmatically.
Downloaded is not enabled: the Safari extension funnel
Write the funnel down before you instrument anything. For a typical Safari web extension it has six steps:
- Impression on the App Store (App Store Connect).
- Product page view (App Store Connect).
- Download of the app (App Store Connect).
- Containing app opened, where most extensions show setup instructions (your app).
- Extension enabled in Safari settings (your app, using the API below).
- Extension active: running in Safari, with website access, on most days (your extension).
Steps 1 to 3 come from Apple. Steps 4 to 6 are yours to measure, and step 5 is where Safari extensions most often leak. A developer on the Apple Developer Forums described the classic need in February 2025: "we plan to show tutorial (WKWebview) only in case user has safari extension disabled." At the time, an Apple engineer replied that "There's no supported way for you to know if your extension is disabled from an iOS app." That changed with the 26.2 updates, as Apple staff later confirmed in another forum thread.
Check whether your Safari extension is enabled from the containing app
SFSafariExtensionManager is the class your app uses to read the state of its own extension. Apple's documentation says that in macOS it works for Safari app extensions and Safari web extensions, and "In iOS and visionOS, use this class to find out the current state of a Safari web extension." The iOS method, getStateOfExtension(withIdentifier:completionHandler:), is available from iOS, iPadOS, Mac Catalyst and visionOS 26.2. On macOS the older getStateOfSafariExtension(withIdentifier:completionHandler:) has existed since macOS 10.12. Both return an SFSafariExtensionState with an isEnabled Boolean.
A small helper that works on both platforms and returns nil when the system cannot tell you:
import SafariServices
// The bundle identifier of the extension target, not of the app.
let extensionID = "com.example.Highlighter.Extension"
/// true or false when the system can tell us, nil when it cannot.
func isSafariExtensionEnabled() async -> Bool? {
#if os(macOS)
let state = try? await SFSafariExtensionManager.stateOfSafariExtension(withIdentifier: extensionID)
return state?.isEnabled
#else
if #available(iOS 26.2, visionOS 26.2, *) {
let state = try? await SFSafariExtensionManager.stateOfExtension(withIdentifier: extensionID)
return state?.isEnabled
}
return nil // Older systems: unknown, so show the setup guide.
#endif
}
Use the result in two ways:
- Onboarding. If the extension is off, show the setup steps. On iOS 26.2 and later,
SFSafariSettings.openExtensionsSettings(forIdentifiers:)"Presents the extensions pane from Safari Settings", which saves users from hunting for the toggle. Apple notes that it "returns an error unless you call it while your app is in the foreground." On macOS,SFSafariApplication.showPreferencesForExtension(withIdentifier:)does the same job. - Measurement. When the app comes to the foreground, record whether the extension is enabled, together with an anonymous install ID and the app version. Divide "enabled" by "app opened" and you have the activation rate for step 5.
Two limits keep this honest. The check only runs when someone opens the app, so it measures activation, not ongoing use. And on macOS it is not per profile: in a 2023 forum answer, an Apple engineer said the method "checks if the extension is turned on in any profile."
Measure active users from the extension itself
For step 6 you need a signal from the extension, because only the extension knows it is running in Safari. The pattern is the same minimal ping described in our guide to measuring extension retention and uninstalls: a random install ID, one event on install and update, and at most one heartbeat per day. Every API used here is supported in Safari according to MDN's compatibility data: runtime.onInstalled and runtime.onStartup since Safari 14 on macOS and Safari 15 on iOS, and alarms and storage.local on the same versions.
// background.js: one anonymous heartbeat per day. Works in Safari, Chrome and Firefox.
const ENDPOINT = 'https://api.example.com/v1/ping';
const BROWSER = 'safari'; // set per build
async function getInstallId() {
const { installId } = await browser.storage.local.get('installId');
if (installId) return installId;
const id = crypto.randomUUID();
await browser.storage.local.set({ installId: id });
return id;
}
async function ping(event) {
const today = new Date().toISOString().slice(0, 10);
const { lastPingDay } = await browser.storage.local.get('lastPingDay');
if (event === 'heartbeat' && lastPingDay === today) return;
try {
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
id: await getInstallId(),
event, // 'install', 'update' or 'heartbeat'
version: browser.runtime.getManifest().version,
browser: BROWSER,
day: today,
}),
});
if (res.ok) await browser.storage.local.set({ lastPingDay: today });
} catch {
// Offline or blocked: try again on the next alarm.
}
}
browser.runtime.onInstalled.addListener(({ reason }) => ping(reason));
browser.runtime.onStartup.addListener(() => ping('heartbeat'));
browser.alarms.create('heartbeat', { periodInMinutes: 360 });
browser.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'heartbeat') ping('heartbeat');
});
A few Safari-specific details:
- Keep the install ID in
storage.local. MDN notes that Safari "does not sync items saved in thesyncstorage area", so it behaves like local storage anyway. Each device gets its own ID, which is what you want for counting devices. - Do not add your analytics domain as a host permission. In Safari, website access is something the user grants. Serve the ping endpoint with
Access-Control-Allow-Origin: *instead; it takes no cookies and returns nothing sensitive. - Send nothing about pages. No URLs, titles or page content. The ping only proves the extension is alive.
On the server, distinct IDs per day give you daily active extensions, and distinct IDs over seven days give you a number you can put next to the Chrome Web Store's weekly users. Group heartbeats by version and you also see how fast an update reaches people, which matters because Safari extensions update with their app.
Why runtime.setUninstallURL does not work in Safari
On Chrome and Firefox, runtime.setUninstallURL opens a page when the extension is removed, which is the usual way to ask why someone left. MDN's compatibility data for runtime.setUninstallURL lists it for Safari with the note "API exists, but has no effect." Calling it does not throw, so this is easy to miss.
Measure churn in Safari from two sources instead:
- Heartbeat silence. An install ID that has not pinged for 14 days is lapsed. It may have been disabled, deleted, or its owner stopped using Safari; you cannot tell which, so report it as one number.
- App Store deletions. The Deletions metric in App Store Connect counts app deletions among users who share analytics, which approximates removal of the extension together with its app.
Privacy rules for Safari extension analytics
Apple's rules apply to the whole app, extension included. The App Review Guidelines say in 5.1.2 that "Unless otherwise permitted by law, you may not use, transmit, or share someone's personal data without first obtaining their permission." Guideline 4.4.2 adds that Safari extensions "should not claim access to more websites than strictly necessary to function."
In practice:
- Declare the heartbeat in your app's privacy details and privacy policy, even though it is anonymous.
- Use a random ID, never an Apple account identifier, email or device fingerprint.
- Add an opt-out in the containing app or the extension's settings page. If the extension also ships to Firefox, the same ping must be declared under Firefox's consent rules, which our guide to Firefox data collection consent covers.
- Keep raw events for a short period and aggregate them.
Put Safari next to Chrome, Edge and Firefox
Each store counts something different, so compare like with like:
| Store | Official user number | Uninstall signal |
|---|---|---|
| Chrome Web Store | Weekly users on the listing and in the dashboard | Dashboard uninstalls, setUninstallURL |
| Edge Add-ons | Partner Center reports, explained in our Edge add-on analytics guide | setUninstallURL |
| Firefox Add-ons | AMO statistics, explained in our Firefox add-on stats guide | setUninstallURL |
| Safari (App Store) | None for the extension; app downloads and opt-in usage | App deletions, heartbeat silence |
Your own heartbeat is the one number that is defined the same way everywhere, which makes it the right basis for a cross-browser chart. The store numbers are still what users and competitors see, and they move on their own schedule. Our guide to tracking one extension across Chrome, Edge and Firefox shows how to read those listings side by side, and the overview of Chrome extension analytics tools explains where product analytics fits.
For the public side, Extenify is a cross-platform extension dashboard for browser extension analytics across stores: add the App Store listing of your Safari extension's app next to its Chrome Web Store, Edge Add-ons and Firefox Add-ons listings, and it records ratings, reviews and versions every day and alerts you by email, Slack, Discord, Telegram or Microsoft Teams when they change. If you want those alerts in a team channel of your own, our guide to sending Discord webhook alerts from Node.js shows the plumbing. Every account starts with a 7-day free trial, and the pricing page lists the plans.
Frequently asked questions
How many users does my Safari extension have?
No Apple dashboard tells you directly. App Store Connect reports downloads of the containing app and, for users who opt in, sessions and active devices of that app. To count people who actually run the extension, send an anonymous daily heartbeat from the extension and count distinct install IDs per day or week.
Does App Store Connect show Safari extension usage?
Apple's metric definitions describe app usage (sessions, active devices, installations, deletions, crashes) and do not mention app extensions. Treat those numbers as usage of the containing app, not of the extension in Safari.
Can an iOS app check whether its Safari extension is enabled?
Yes, starting with iOS, iPadOS and visionOS 26.2. Call SFSafariExtensionManager.getStateOfExtension(withIdentifier:completionHandler:) with the extension's bundle identifier and read isEnabled. On earlier iOS versions there is no supported way, so show the setup guide to everyone. On macOS, getStateOfSafariExtension(withIdentifier:completionHandler:) has been available since macOS 10.12.
Does runtime.setUninstallURL work in Safari?
No. MDN's browser compatibility data marks it as partially implemented in Safari with the note "API exists, but has no effect." Use heartbeats that stop arriving and App Store deletions to estimate churn.
Do I need to disclose analytics in a Safari extension?
Yes. Apple's guidelines require permission before you use or transmit personal data and a privacy policy for every app. An anonymous heartbeat with a random ID is low risk, but declare it in your App Privacy details and privacy policy, and give users a way to turn it off.
Summary
Safari extension analytics needs three layers. App Store Connect covers the top of the funnel: impressions, page views, downloads and, for users who opt in, usage of the containing app. The containing app can now tell you whether the extension is enabled on macOS and, since 26.2, on iOS, iPadOS and visionOS, which is the step where most Safari extensions lose people. The extension itself provides the only true usage number, through a small anonymous heartbeat, and because setUninstallURL has no effect in Safari, that heartbeat is also how you measure churn. Keep the telemetry minimal and declared, and compare Safari with other stores using numbers that mean the same thing.
Image credits
- Cover photo: Compass rose by Javier Casares on the WordPress Photo Directory, CC0.


