Appearance
Swift
The SimpleDataLogs Swift package tracks events from macOS and iOS apps. It has no dependencies, uses Swift 6 strict concurrency, and keeps a crash-safe outbox on disk, so an event is never lost to a crash or a closed laptop.
- macOS 13+ and iOS 16+.
- Swift Package Manager.
- Ships a privacy manifest (
PrivacyInfo.xcprivacy) for App Store review.
Install
In Xcode, choose File → Add Package Dependencies…, enter https://github.com/XKD-Labs/simpledatalogs-swift, and add the SimpleDataLogs library to your app target.
Or in Package.swift:
swift
dependencies: [
.package(url: "https://github.com/XKD-Labs/simpledatalogs-swift", from: "0.4.0"),
],
targets: [
.target(name: "MyApp", dependencies: [.product(name: "SimpleDataLogs", package: "simpledatalogs-swift")]),
]Early access
The package URL is published with the public launch. Until then, your invitation includes the package source: add it with Add Local… in Xcode, or .package(path: "…") in Package.swift.
Quick start
swift
import SimpleDataLogs
// At launch: the opt-out first, then identity and default properties, then the key.
SimpleDataLogs.setEnabled(userAllowsUsageStatistics)
SimpleDataLogs.setDeviceID(anonymousDeviceID)
SimpleDataLogs.setDefaultProperties(["app_version": "1.1.0", "distribution": "website"])
SimpleDataLogs.configure(ingestKey: "sdl_pub_…")
// Anywhere, from any thread or actor; it returns at once.
SimpleDataLogs.track("screenshot_taken", properties: ["type": "area"])Use a public key (sdl_pub_…) in apps. The SDK logs a warning if it sees a secret key.
Autocapture
One argument sends launches, sessions and crashes without any track call. It is off until you pass it.
swift
SimpleDataLogs.configure(ingestKey: "sdl_pub_…", autocapture: .all)| Option | What you get |
|---|---|
.appLaunches | app_launched on every launch, with is_first_launch |
.sessions | session_start, a session_id on every event, and session_end (duration_s) when the session ends; see session modes |
.crashes | An uncaught NSException is saved at the crash and sent as app_crashed (exception_name, reason, stack) on the next launch |
.all is all three; pass a set to pick, e.g. [.sessions, .crashes].
Session modes
sessionMode: | A session ends after | Suits |
|---|---|---|
.activation (default) | 30 minutes away; macOS counts activation, iOS foreground and background | Regular windowed and iOS apps |
.inactivityTimeout(minutes: 30) | That many minutes without a track call | Menu-bar and accessory apps, which are rarely frontmost |
swift
SimpleDataLogs.configure(ingestKey: "sdl_pub_…", autocapture: .all, sessionMode: .inactivityTimeout(minutes: 30))- In
.inactivityTimeout, the firsttrackafter a quiet gap sendssession_end(stamped at the last activity) andsession_startbefore the event itself. - Back-dated events never move sessions.
- An app quit mid-session ends that session on the next launch, at its last activity.
Crash reasons
By default the reason is sent word for word, up to 4,000 characters. Reasons can carry file paths, record ids or emails, so you can redact them:
swift
SimpleDataLogs.configure(ingestKey: "sdl_pub_…", autocapture: .all, crashReasonRedaction: .standard).standardreplaces digits with#, UUIDs and hex values with<uuid>and<hex>, paths and file URLs with<path>, and emails with<email>, then cuts the reason at 200 characters.Pick parts with
CrashReasonRedaction(shouldStripNumbers:shouldStripPaths:shouldStripEmails:maxLength:).File paths in
stackframes are always cut to the file or module name.Screen views are opt-in per screen:
.trackScreenView("Settings")on a SwiftUI view, orSimpleDataLogs.trackScreen("Settings")from UIKit/AppKit. Both sendscreen_viewwith ascreenprop.Swift runtime traps (
fatalError, force unwraps) and signals are notNSExceptions, so they are not captured.The opt-out applies: with
setEnabled(false), nothing is captured and a saved crash is discarded.
Active and background
Every event tracked now says whether someone was using the app, so the Sessions tab can tell active sessions from passive ones:
app_stateisactivewhile the app is in front, or on a Mac when there was keyboard or mouse input in the last 10 seconds (a menu-bar hotkey counts); otherwisebackground.engaged_msis the time the app spent in front since the previous event.- Mark background work yourself with
track("sync_finished", properties: ["app_state": "background"]); your value wins. - Turn it off with
configure(ingestKey: …, activity: .off). Back-dated events carry neither.
API
| Call | What it does |
|---|---|
configure(ingestKey:endpoint:autocapture:sessionMode:crashReasonRedaction:) | Starts the SDK once; autocapture defaults to none. endpoint defaults to https://edge.simpledatalogs.com; later calls are ignored |
trackScreen(_:properties:) | Records screen_view with a screen prop. |
track(_:properties:) | Records one event. event_id and ts are stamped now, when the event happens |
track(_:properties:at:eventID:) | Records an event that happened earlier; see back-dated events |
track(_:properties:eventID:) | Records an event now with your own event_id, so a resend is counted once |
setDeviceID(_:) | Sets, or clears with nil, the device_id on every later event |
setDefaultProperties(_:) | Replaces the properties merged into every event; the event's own properties win on a clash |
setEnabled(_:) | The opt-out. false ignores new events and deletes every unsent event; true collects again |
flush() async | Sends everything now and returns when that attempt ends. Undelivered events stay on disk |
disable() | The same as setEnabled(false) |
flushAndDisable(timeLimit:) | Ignores new events at once, sends what is queued (10 s by default), then turns collection off and deletes the rest. Async, or with a completion: callback |
Properties
PropertyValue is a string, integer, double or boolean, and literals work directly:
swift
SimpleDataLogs.track("export_finished", properties: ["format": "png", "duration_ms": 412, "is_pro": true])Coming from Firebase or another loosely typed API? PropertyValue(bridging:) converts an Any, and returns nil for anything else:
swift
SimpleDataLogs.track(eventName, properties: parameters.compactMapValues(PropertyValue.init(bridging:)))NaN and infinity are dropped before the event is stamped.
Back-dated events
Keep events from before the user agreed to share usage, such as onboarding steps, and send them later with their real time:
swift
for step in savedOnboardingSteps {
SimpleDataLogs.track("onboarding_step", properties: ["step": .string(step.name)], at: step.completedAt, eventID: step.id)
}atmay be up to 3,650 days back, inside the ingest API's 10-yeartswindow. Older events are dropped with a log line; a future time is sent as now.eventIDis optional. Pass a stable UUID when the same event might be sent twice, so it counts once.
The opt-out
Call setEnabled(_:) before configure. A user who opted out then never sends a byte, and whatever an earlier launch left on disk is deleted instead of sent.
swift
SimpleDataLogs.setEnabled(settings.isUsageSharingOn)
SimpleDataLogs.configure(ingestKey: "sdl_pub_…")
// Later, when the user flips the toggle:
SimpleDataLogs.setEnabled(newValue)The SDK doesn't persist the choice. Your app's setting is the source of truth, so pass it on every launch.
To send what was already collected before stopping, use flushAndDisable() instead:
swift
await SimpleDataLogs.flushAndDisable()It ignores new events at once, skips any pending backoff for one last send, then deletes whatever did not make it within the time limit.
How it behaves
Batching
Events go out in batches of 50, or 10 seconds after the first event of a partial batch (5 seconds in debug builds). Every request stays under 256 KB.
A crash-safe outbox
- Each event is appended to a journal on disk as it is tracked.
- Batches are sealed into files and deleted only after a
202. - The next launch resends anything left over, including a journal cut short by a crash.
- Files live in
Application Support/<bundle id>/SimpleDataLogs/(inside the container for sandboxed apps) and are excluded from backups. - On iOS, the files use the
completeUntilFirstUserAuthenticationprotection class, so background work on a locked device can still track and send.
Lifecycle
The SDK flushes when the app terminates and when the Mac goes to sleep (macOS), or when the app moves to the background (iOS). On iOS it asks for background time for that send, for at most 20 seconds. Anything a flush can't deliver in time stays on disk for the next launch.
Events tracked in applicationWillTerminate are kept and usually sent before the app quits:
- Once termination begins, the SDK holds the main thread until every queued event is in the journal (at most 1 second).
- Then it sends them, holding the quit until the send ends or 2 seconds have passed since termination began.
- Before termination,
tracknever waits.
Errors and offline
| Answer | What the SDK does |
|---|---|
202 | Deletes the batch file |
401 | Pauses sending and logs one configuration error. Re-probes one minute later, then every 10 minutes, and on the next launch. A new key that needs ~30 s to go live is picked up by the first re-probe |
413 | Splits the batch in half and resends. A single event still too large is dropped, with a log line |
415 | Sets the batch aside as .unsupported and moves on |
| Anything else, a timeout, offline | Backs off with full jitter from 1 second to 5 minutes, honoring Retry-After |
Caps
- The outbox keeps at most 10,000 events, 10 MB or 500 batch files. Past that, the oldest batches are dropped, with a log line.
- If more than 5,000 events wait to be written (a hot loop faster than the disk), new ones are skipped, with one log line, until it catches up.
Nothing throws or crashes your app.
Debugging
The SDK logs through os.Logger with the subsystem com.simpledatalogs.sdk:
sh
log stream --predicate 'subsystem == "com.simpledatalogs.sdk"' --level debugFor local development, point it at a local ingest:
swift
SimpleDataLogs.configure(ingestKey: "sdl_pub_…", endpoint: URL(string: "http://localhost:4001")!)Sandboxed Mac apps
A sandboxed app needs the com.apple.security.network.client entitlement to send.
Next steps
- Core concepts: one deployment per build, such as
macosandmacos-appstore. - Privacy for senders: device ids, not emails.
- Prompt recipes: have your coding agent wire it up.