Public beta week one: what Sentry caught and what shipped

Friday morning 2 weeks ago, the beta went out on TestFlight. By Saturday lunchtime Sentry was loud, Linear was filling up, and I had the first concrete answer to "does it work on machines that aren't mine."
Five days later, hora Calendar is on 0.6.1 build 89, with three patch releases since launch, roughly two dozen issues closed, and the worst hangs already in the rear-view mirror. This is what week one of public beta actually looked like — the stats, the failures, the fixes, and what I'm carrying into 0.7.

The week in numbers
- 0.6.0 build 71 → 0.6.1 build 89 in five days. Three patch releases chasing real-world bugs.
- +24 Linear issues closed since the beta dropped — bugs, polish, and a handful of small features.
- Eight distinct Sentry issue groups triaged in the first 48 hours, four of them in the same SwiftData cluster.
- App-side telemetry: hora doesn't phone home with usage analytics. The truth about what's running on people's Macs comes from Sentry — and from Discord.
That last point matters: I deliberately don't ship product analytics in the app. A Mac calendar is not a web product. The user gets to assume that nothing about their schedule is leaving their machine for me. So when I say "the app is hanging on N machines," I mean Sentry told me. There is no DAU dashboard.
Sentry landed two days before TestFlight
The single best decision I made the week before launch was wiring up Sentry on April 25, two days before the beta went out, instead of "after launch when there's time." It caught the four worst hangs within 24 hours of the first installs.

The setup is small: the Sentry Cocoa SDK in the app, a post-build script in Xcode Cloud that uploads dSYMs, release tracking tagged with MARKETING_VERSION+CI_BUILD_NUMBER so I can tell which build a hang came from. The full integration took a day. The first hang report came in within hours of the first install.
The SwiftData lesson — four hangs, same shape
By Sunday afternoon I had a pattern. Four separate Sentry issues — HORA-APP-Z, HORA-APP-15, HORA-APP-19/1C, HORA-APP-1V/1W/20 — all said the same thing: App Hanging for at least 2000 ms, all on the main thread, all in code that touched my SwiftData @Query.
The shape was identical across the four:
- A SwiftUI view holds a broad
@QueryforEvent(e.g. all events, or all events for the next N days). - A computed property iterates them and reads
attendeesJSON(aString?storing the JSON-encoded attendees array). - Iterating + reading that property faults each
Eventmodel into memory and decodes the JSON. - With 30 events on my dev account: invisible. With 3,000 events on a real calendar: a half-second of main-thread work every render.
The worst offender, WidgetDataExporter.exportEvents, was annotated @MainActor and faulted all events to filter for the next 48 hours. The fix:
// Before: @MainActor exporter, faults every event to find the next 48h
@MainActor
final class WidgetDataExporter {
func exportEvents(modelContext: ModelContext) {
let allEvents = try modelContext.fetch(FetchDescriptor<Event>())
let window = allEvents.filter { /* next 48h */ }
// ... encode JSON, write App Group files ...
}
}
// After: detached, dedicated ModelContext, windowed predicate
final class WidgetDataExporter {
func exportEvents(container: ModelContainer) async {
let accounts = await MainActor.run { /* capture small main inputs */ }
await Task.detached(priority: .utility) {
let context = ModelContext(container)
let now = Date()
let end = now.addingTimeInterval(48 * 3600)
let descriptor = FetchDescriptor<Event>(
predicate: #Predicate { $0.startDate >= now && $0.startDate <= end }
)
let events = try context.fetch(descriptor)
// ... encode JSON, write App Group files ...
}.value
await MainActor.run { WidgetCenter.shared.reloadAllTimelines() }
}
}Same pattern applied to SidebarModePickerWithBadge, the NotificationService cluster (pendingInvitationIDs, scheduleInvitationNotifications, updateDockBadge), and InvitationsView. The latter also got a tighter @Query — attendeesJSON != nil — so the iteration domain shrinks from ~3,000 events to the few hundred actually carrying invites.
The lesson, written down so I don't forget: @MainActor on a function whose body iterates @Model instances is a code smell. SwiftData faults are I/O. I/O on main is a hang waiting for someone with a big enough calendar.
Launch-time hang: open SwiftData off-main
The most painful issue of the week was HORA-APP-13 — 95 events from 3 users in a single day, hanging on HoraApp.$main. A launch-time hang. The first thing those three users saw of hora was a beachball.
Cause: opening the SwiftData ModelContainer synchronously on a cold disk with a large store. Fix: open it in a Task, show a tiny launch splash while it warms up, only mount the main scene once the container is ready. Cosmetic-looking, but the difference between "the app is broken" and "the app is loading" is the whole product on first launch.
Google's edge cases — auth and rate limits
Two more Sentry findings worth their own mention:
HORA-APP-R— 403 Insufficient authentication scopes, 160 events from 2 users. The OAuth grant succeeded, but a follow-up call asked for a scope the user hadn't approved. Fix: detect the specific 403 reason and re-prompt with the missing scope, instead of treating it as a generic API error.HORA-APP-S— quota exceeded on the Google Calendar API. Two flavors of 403 (rateLimitExceededvs per-userQueries per minute per user) needed different responses. Fix: exponential backoff with jitter, plus a token bucket so we stop hammering the same endpoint even when retries succeed.
These are the kind of bugs that don't exist on a developer's machine because I never re-grant scopes or burn quota. They show up the moment a real user has two Google accounts and a calendar that mutates faster than my tests assume.
Polish, not just hangs
The other half of the week was small things that add up:
- Escape dismisses everything — sheets, the Go-to-Date overlay, the search panel. (
SZA-153,SZA-129) - Menu-bar shows "ends in 14m" while a meeting is in progress, instead of vanishing the moment the meeting starts. (
SZA-157) - Switching video-link provider (Meet → Zoom → Custom) now clears the stale link, and
conferenceData: nullis sent to Google so the old Meet doesn't ghost-resurrect on next sync. (SZA-149) - Instant local refresh after CRUD — the calendar grid updates the moment a write completes, not on the next sync tick. The RRULE parser was rewritten component-wise so DAILY/WEEKLY presets round-trip cleanly. (
SZA-135) - Widget deep-links route through
AppDelegateand drain on first appear, so tapping a widget event after a cold launch lands you on the right event instead of the dashboard. (SZA-137) - Discord link in About, Settings… in the dock menu, and a rebranded launch splash that finally matches the rest of the app's look. (
SZA-130/257,SZA-250) - Push channel cleanup on sign-out — the live
events.watchchannels now get an explicitevents.stopbefore the OAuth token is deleted, so Google doesn't keep pushing to a dead Mac for the channel's remaining TTL. (SZA-181) - Drop focus-triggered sync — the focus-on-window sync was a pre-push fallback; now that APNs push is live, it's a redundant API call every time you tab back. Removed. (
SZA-136)
What didn't ship
Being honest about what the week didn't get to:
- 0.7.0 work paused. Apple Intelligence quick-add, focus-time planning, in-app calendar CRUD — all sitting on a branch. Stabilization comes first.
- Translations are still mostly machine-generated. I got some help with German — that's the unblock I'm most excited about.
What I'm carrying into next week
- Finish the 0.6.x stabilization tail. A handful of WIP fixes from the 0.6.1 batch are still in flight.
- Ramp the second 200-tester batch. The first wave was ~200; the horacal.app/testflight link is live for the next batch.
- Resume 0.7.0 work mid-week, once Sentry's been quiet for 48 hours straight.
The takeaway
You only see your own calendar in dev. Production has 3,000-event accounts, expired OAuth grants, multiple Google sign-ins, and all the weird shapes of real life. Two days of Sentry instrumentation before launch saved me an entire week of "one user reported…" guesswork — and gave me concrete stacks to fix instead of vibes.
If you're shipping a TestFlight beta this month, wire Sentry (or your equivalent) in before you go out the door. The flight time from "first install" to "first hang" is shorter than you think.
Join the beta community
Daily builds. Direct line to me.
Other early testers.
Discord is where feedback turns into fixes. That's where I'll be every day for the next few weeks.
Join the Discord →discord.gg/8JFz4FfBGQ
If you're already on the beta, thanks for finding the rough edges. If you're not, the waitlist is still open and the second 200-tester batch lands this week.
Follow the build at @moto_szama, check out hora Calendar, or reach out at hello@horacal.app.