Zero-SDK iOS Analytics: How We Ship Without Firebase, Mixpanel, or Sentry

This is not a privacy manifesto. It's a practical guide to shipping iOS apps without any third-party analytics, crash reporting, or tracking SDKs — and still knowing what's happening.

Series: Local-first + AI + Privacy on iOS

  1. Local-first iOS with SwiftData and CloudKit
  2. Using Claude API in a Swift app
  3. Zero-SDK iOS analytics (this post)

Why zero-SDK?

Every iOS analytics tutorial starts the same way: add Firebase, drop in a few logEvent calls, and you're done. It's convenient. It's also a decision that affects your app's binary size, launch time, privacy posture, and compliance obligations for the rest of its life.

NoDiary, Fed?, and Symptio all ship with zero third-party dependencies. No Firebase Analytics, no Mixpanel, no Sentry, no Crashlytics, no Amplitude — nothing. The dependency list in each project is empty. This wasn't an ideological decision. It started as laziness (one fewer CocoaPod to manage) and turned into a deliberate strategy once we saw the trade-offs clearly.

The question everyone asks: "How do you know what's happening in your app?" The answer is that Apple gives you more than you think, and for the few things it doesn't cover, you can build surprisingly effective lightweight alternatives.

What App Store Connect gives you for free

Most developers underestimate App Store Connect's analytics. If you've never spent time in the App Analytics tab, you're leaving free insights on the table. Here's what you get without writing a single line of code or adding any SDK:

Acquisition and engagement

All of these metrics are aggregated and anonymized by Apple. You never see individual user data. You can't identify a specific person or track their behavior across sessions. Apple computes the aggregates on their end and shows you the result.

Crash reports

App Store Connect provides symbolicated crash logs with full stack traces, grouped by OS version and device type. It's not Sentry — you don't get breadcrumbs, custom context, or real-time Slack alerts. But it covers roughly 90% of what an indie developer needs. You see which crashes are most common, which OS versions are affected, and where in your code the crash occurred.

The big trade-off: App Store Connect data has a 24-48 hour delay. You learn about a crash spike a day after it happens. If you ship a catastrophic bug on Monday, you won't see the crash data until Wednesday. For a small team shipping focused apps, this delay is acceptable. For a high-traffic app where minutes matter, it isn't.

Peer group benchmarks

Apple shows how your retention and conversion rates compare to similar apps in your category. This is surprisingly useful. If your Day 7 retention is 35% and the peer group average is 25%, you know you're doing something right — without needing a dedicated analytics team to establish that baseline.

What's missing

App Store Connect does not provide funnel analysis, custom event tracking, user segmentation, A/B testing, or real-time dashboards. If you need to know "what percentage of users who open the settings screen also enable iCloud sync," App Store Connect can't tell you. You need to accept this gap or fill it with something else.

CloudKit aggregate counters

For the few metrics that matter beyond what App Store Connect provides, you can build lightweight anonymous counters using CloudKit's public database. This doesn't require any third-party infrastructure — it uses the same CloudKit you're already using for sync.

The concept

CloudKit's public database is readable by all users and writable by authenticated iCloud users. You can create a shared CKRecord that acts as a simple counter. Each app instance increments the counter when a meaningful event occurs. No user ID, no device ID, no timestamp per event — just a number that goes up.

For example, Fed? tracks "total feeding events across all users" as a single counter in the public database. This tells us that the app is being used and gives a rough sense of engagement growth, without identifying any individual user or household.

// Anonymous aggregate counter using CloudKit public database
import CloudKit

func incrementFeedingCounter() async {
    let publicDB = CKContainer.default().publicCloudDatabase
    let recordID = CKRecord.ID(recordName: "global_stats")

    let statsRecord: CKRecord
    do {
        statsRecord = try await publicDB.record(for: recordID)
    } catch {
        // First launch or record doesn't exist yet — create it
        statsRecord = CKRecord(recordType: "AggregateStats", recordID: recordID)
    }

    let current = statsRecord["totalFeedings"] as? Int64 ?? 0
    statsRecord["totalFeedings"] = (current + 1) as CKRecordValue

    do {
        try await publicDB.save(statsRecord)
    } catch {
        // Silently drop — an approximate counter missing one
        // increment is fine for aggregate anonymous metrics
    }
}

This uses the modern async/await variants of CKDatabase.record(for:) and CKDatabase.save(_:), which are cleaner and avoid nested completion handlers. The code is sequential and easy to follow.

Honest caveat — race condition: This is an approximate counter. If two users call incrementFeedingCounter() at the same instant, both read the same value, both write current + 1, and one increment is lost. For aggregate anonymous metrics — "roughly how many feedings happened" — this level of precision is sufficient. If you need exact counts, you'd need an atomic server-side increment, which CloudKit doesn't natively provide.

Privacy advantage: Even if someone gained access to the public database, there's no way to trace a count back to a specific user. The record contains a single integer. No device fingerprints, no IP addresses, no user IDs — just a number.

Limitations

This approach gives you counters and nothing else. If you need histograms, cohort analysis, or time-series data, a single counter won't scale. You could create daily counters (one record per day), but at that point you're building your own analytics backend on CloudKit — which is possible but defeats the simplicity argument. We keep it to a handful of aggregate counters and lean on App Store Connect for everything else.

Opt-in feedback without a server

Numbers tell you what's happening. They don't tell you why. For qualitative feedback, we use two channels — both of which require zero backend infrastructure.

App Store reviews via StoreKit

Apple's SKStoreReviewController (or requestReview() in StoreKit 2) lets you prompt the user for an App Store review. Apple throttles this to three prompts per year per app, and the system may choose not to show the prompt at all. You can't control the timing precisely, but you can control when you ask.

The trick is to ask at the right moment. Don't ask on first launch. Don't ask after a crash. Don't ask randomly. Ask after a positive interaction — after the user has demonstrated genuine engagement with the app.

// Request review after meaningful engagement
import StoreKit

final class ReviewManager {
    @AppStorage("appLaunchCount") private var launchCount = 0
    @AppStorage("totalEntries") private var totalEntries = 0
    @AppStorage("lastReviewRequest") private var lastRequest: Double = 0

    func requestReviewIfAppropriate() {
        launchCount += 1

        // Require 7+ days of use and meaningful engagement
        let dominated = launchCount >= 7 && totalEntries >= 10
        let cooldown = Date().timeIntervalSince1970 - lastRequest > 90 * 86400

        guard dominated && cooldown else { return }

        if let scene = UIApplication.shared.connectedScenes
            .first(where: { $0.activationState == .foregroundActive })
            as? UIWindowScene {
            AppStore.requestReview(in: scene)
            lastRequest = Date().timeIntervalSince1970
        }
    }
}

For NoDiary, we trigger the review prompt after the user has created at least 10 diary entries and used the app for 7 or more days. For Fed?, it's 20 feedings and a week of use. This produces higher-quality reviews from people who have actually experienced the app, rather than confused one-star reviews from people who just installed it.

Email-based feedback

For direct feedback, we use a simple in-app "Send Feedback" button that composes a mailto: link with a pre-filled subject line and basic device information in the body. The user sends an email from their own mail app. They see exactly what they're sending before they send it. No hidden telemetry, no background uploads, no server to maintain.

This sounds primitive, and it is. But the emails we receive are significantly more useful than any analytics event could be. Users write full sentences explaining what confused them, what they wish worked differently, what they love. A single thoughtful email is worth more than a thousand button_tapped events in a Mixpanel funnel.

Binary size and launch time

The most underrated benefit of zero SDKs has nothing to do with privacy or data philosophy. It's about what happens when you compile your app.

Your app is tiny

NoDiary's IPA is under 5 MB. That's the entire app — UI, assets, logic, everything. Compare that to the SDK overhead alone for a typical app: Firebase Analytics adds roughly 4 MB, Crashlytics adds another 3 MB, and Remote Config adds 1-2 MB. Those three SDKs — before you write any of your own code — account for 8-12 MB of binary size.

Binary size matters more than most developers think. App Store Connect data shows that conversion rate drops measurably for apps over 200 MB (the threshold where cellular download requires explicit confirmation). A 5 MB app versus a 15 MB app won't hit that ceiling, but the principle scales: less code means fewer bugs, faster launches, and less surface area for things to go wrong.

Launch time

Every framework your app links against adds time to the pre-main phase of launch. The dynamic linker (dyld) needs to load, rebase, and bind each framework before your main() function ever runs. Fewer frameworks means a shorter pre-main time.

We measured NoDiary's cold launch at approximately 180ms on an iPhone 14. Apps with Firebase typically report 200-400ms added to cold launch just from SDK initialization — creating network sessions, reading cached config, registering observers, spinning up background dispatch queues. The Firebase FirebaseApp.configure() call alone takes measurable time on older devices.

The SDK tax compounds

Each SDK you add doesn't just cost its own weight. SDKs register notification observers, schedule background tasks, write to disk on launch, and sometimes perform network requests before your app's first view controller appears. Even SDKs marketed as "lightweight" have hidden initialization costs.

With zero SDKs, your didFinishLaunchingWithOptions does exactly what you wrote and nothing else. There are no surprises, no background work you didn't initiate, no threads you don't own. The app is entirely yours.

A useful mental model: Every SDK is a co-tenant in your process. They share your memory budget, your CPU time, and your crash rate. The fewer co-tenants, the more predictable your app's behavior.

When zero-SDK doesn't work

Honesty is important here. This approach has real limitations, and pretending otherwise would be misleading.

Real-time crash alerting. If you need to know about a crash within minutes — not hours — you need a dedicated crash reporting service. Building your own log-shipping pipeline is possible but adds the very infrastructure complexity that zero-SDK is meant to avoid.

Growth experiments. If you're running A/B tests on onboarding flows or testing pricing strategies, you need event-level analytics with user segmentation. App Store Connect can't tell you where users drop off in a multi-step flow.

Team access. App Store Connect's analytics sharing is limited. If you have a large team where product managers, designers, and engineers all need metric dashboards, the single-login nature of App Store Connect becomes a bottleneck.

Our approach works because of our context: a small team shipping focused apps where privacy is the product feature, not an afterthought. NoDiary has "zero third-party SDKs" as a selling point on its product page. Fed? lists "zero data collection" as a headline feature. The absence of tracking is part of the value proposition.

If analytics is your competitive advantage — if optimizing conversion funnels and running growth experiments is how your business works — don't go zero-SDK. Use the tools that serve your goals.

The compliance upside

One benefit worth calling out explicitly: zero-SDK means zero compliance burden for third-party data processors. Under GDPR, every third-party SDK that collects data is a data processor you need to disclose, audit, and potentially negotiate a Data Processing Agreement with. With zero SDKs, your privacy policy becomes dramatically simpler. NoDiary's privacy policy is essentially: "We don't collect any data. There is no data to protect." That's not just a privacy feature — it's an engineering simplification that eliminates an entire category of legal and operational overhead.

Conclusion

Zero-SDK is not about ideology. It's about choosing the right trade-offs for your product and your users. For our apps, the trade-offs are clear: we give up real-time crash alerts, funnel analysis, and user segmentation. In return, we get smaller binaries, faster launches, simpler privacy policies, zero compliance overhead, and the ability to truthfully tell our users that no third party ever sees their data.

The tools exist to make this work. App Store Connect covers acquisition and engagement metrics. CloudKit aggregate counters fill the gaps for the few numbers that matter. StoreKit review prompts and email feedback give you qualitative signal. And the binary size and launch time improvements are a bonus that keeps paying dividends with every release.

If you missed the earlier posts in this series, post one covers local-first architecture with SwiftData and CloudKit, and post two covers integrating the Claude API in a Swift app. Together, they describe the full technical stack behind our apps: local-first data, on-device AI, and zero third-party dependencies.

Series: Local-first + AI + Privacy on iOS

  1. Local-first iOS with SwiftData and CloudKit
  2. Using Claude API in a Swift app
  3. Zero-SDK iOS analytics (this post)