iOS
The Zharp Swift package sends an iOS app's logs, crashes, hangs and errors, tied together by session and user, and traces its screens and network requests.
This SDK is new, and its API may still change before 1.0. Requires iOS 15 or later and Xcode 16 or later (OpenTelemetry Swift needs the Swift 6 tools).
Install
In Xcode: File → Add Package Dependencies, paste the URL and add the Zharp library to your app target.
https://github.com/zharp-io/zharp-ios-lib
Or in Package.swift:
dependencies: [
.package(url: "https://github.com/zharp-io/zharp-ios-lib", from: "0.1.0"),
],
targets: [
.target(name: "MyApp", dependencies: [.product(name: "Zharp", package: "zharp-ios-lib")]),
]Initialize
As early as possible at launch, so crashes and the first screens are caught. The key is safe to ship: it can only send.
SwiftUI
import SwiftUI
import Zharp
@main
struct ShopApp: App {
init() {
Zharp.start { options in
options.key = "zpk_…"
options.service = "shop-ios"
options.environment = "production"
}
}
var body: some Scene {
WindowGroup { ContentView() }
}
}UIKit
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Zharp.start { options in
options.key = "zpk_…"
options.service = "shop-ios"
options.environment = "production"
}
return true
}When it starts it writes one line to the console:
[Zharp] Active: service "shop-ios", environment "production"
Without a key it says so and sends nothing. Every other call does nothing until start.
Logs
Zharp.log.info("Checkout started", ["items": cart.count])
Zharp.log.warn("Slow response", ["ms": 3200])
Zharp.log.error("Payment failed", ["orderId": order.id])
Zharp.log.debug("Cache miss")Each log carries the session, user, device, screen and trace. print and os_log aren't sent.
Logs only
If you only want logs, turn tracing off. Logs, errors, and the user and session they belong to are still sent; no spans are recorded and no trace headers are added to outgoing requests.
Zharp.start { options in
options.key = "zpk_…"
options.service = "shop-ios"
options.tracing = false
}The logs show on the Logs page under the service name, and on the app's Logs tab with the user and session of each line. The key comes from an app — see Get a key. To add tracing later, remove the option; nothing else changes.
Errors
do {
try checkout(cart)
} catch {
Zharp.captureError(error, ["orderId": order.id]) // handled
}Swift errors carry no stack of their own, so a caught error has the stack of the place you reported it from. Errors are sent within a second and never sampled out.
Crashes and hangs
Uncaught Objective-C exceptions and fatal signals are captured — Swift runtime errors arrive as EXC_BREAKPOINT, bad memory access as EXC_BAD_ACCESS. iOS gives a crashing app no time to send anything, so the crash is written to the device and sent the next time the app starts, with the time, session, user, screen and release it happened in. While Xcode's debugger is attached it catches crashes first; to try it, launch the app from the home screen.
When the main thread is blocked for 2 seconds or more, Zharp reports an AppHang with the screen it happened on.
Readable stack traces
Release builds are stripped, so your own frames are addresses. Upload each build's dSYMs and Zharp shows — and groups — crashes and errors by your functions, files and lines, matching each binary image to its dSYM by UUID.
- In Build Settings, set Debug Information Format to DWARF with dSYM File for Release.
- Add a Run Script build phase after the others, with
$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)under Input Files so it runs after the dSYM is written. With User Script Sandboxing on, allow the folder to be read.
# Upload this build's dSYMs to Zharp (release builds only).
if [ "$CONFIGURATION" = "Release" ] && [ -n "$ZHARP_AUTH_TOKEN" ]; then
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" # Xcode doesn't load your shell's PATH
npx --yes @zharp/cli dsym upload "$DWARF_DSYM_FOLDER_PATH" --app <application id> || \
echo "warning: Zharp dSYM upload failed; crashes from this build stay unsymbolicated until it's uploaded"
fiZHARP_AUTH_TOKEN is a workspace API key with the artifacts:write scope — a secret: set it in CI (or, for archives from Xcode, with launchctl setenv), never in the project. See The upload key.
No Node on the build machine
The same with curl — one upload per architecture slice, keyed by its Mach-O UUID:
if [ "$CONFIGURATION" = "Release" ] && [ -n "$ZHARP_AUTH_TOKEN" ]; then
APP_ID=<application id>
URL="${ZHARP_URL:-https://api.zharp.io}/api/v1/applications/$APP_ID/artifacts"
find "$DWARF_DSYM_FOLDER_PATH" -maxdepth 1 -name '*.dSYM' | while read -r dsym; do
for dwarf in "$dsym"/Contents/Resources/DWARF/*; do
xcrun dwarfdump --uuid "$dwarf" | while read -r _ uuid arch _; do
arch="${arch#(}"; arch="${arch%)}"
curl --fail --silent --show-error -H "Authorization: Bearer $ZHARP_AUTH_TOKEN" \
-F kind=dsym -F "debugId=$uuid" -F "arch=$arch" -F "name=$(basename "$dwarf")" \
-F "file=@$dwarf" "$URL" || echo "warning: Zharp dSYM upload failed for $uuid"
done
done
done
fiFor an archive built elsewhere (Xcode → Organizer → Archives → Show dSYMs), run npx @zharp/cli dsym upload <path to .dSYM or folder> --app <id>. To look up one address by hand, atos still works:
atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x100a30000 0x0000000100a3c1f4
Tracing
Screens
UIKit screens are recorded on their own: each view controller of yours that appears records a screen CheckoutViewController span and becomes the current screen on everything after. Container and system controllers are skipped. SwiftUI views all live in one hosting controller, so name them:
CheckoutView()
.zharpScreen("Checkout")
// or from anywhere
Zharp.trackScreen("Checkout")Network requests
Every URLSession request becomes a span with its method, URL, status and duration. To continue a request into your back end as one trace, list your API's hosts — a host, *.domain, host:port or a full origin:
options.tracingOrigins = ["api.shop.com", "*.shop-internal.com"]
Those requests get traceparent and a baggage header with the user and session. No other host gets them. Network spans include the full URL, query string included — keep secrets out of URLs.
Spans of your own
let receipt = try Zharp.startSpan("checkout", attributes: ["items": 3]) {
try checkout(cart)
}
let order = try await Zharp.startSpan("place order") {
try await api.placeOrder(cart)
}
Zharp.setTag("plan", "pro") // on everything from now on; nil removes itSpans you start with the OpenTelemetry Swift API go to Zharp too: the providers are registered globally.
User context
Zharp.setUser(id: user.id, email: user.email) // name: is optional too Zharp.clearUser() // on sign-out: also starts a new session
- The device model (e.g.
iPhone15,2), the iOS version, the session, the user you set, the current screen and your tags go on everything. - An install ID that Zharp creates and keeps in
UserDefaults. It isn't the IDFA or IDFV, needs no tracking permission, and goes away when the app is deleted.
Options
| options. | Default | Notes |
|---|---|---|
key | — | Required. The app key, zpk_…. |
service | The bundle identifier | |
environment | none | |
release | "2.3.0 (412)" | The version and build from the bundle |
endpoint | https://api.zharp.io/api/v1/otlp | |
tracing | true | false is logs only |
tracingOrigins | [] | Hosts that get traceparent and baggage |
sampleRate | 1.0 | Share of traces kept; errors, crashes and logs are always sent |
captureCrashes | true | |
detectHangs / hangThreshold | true / 2 s | |
trackScreens | true | UIKit view controllers |
traceNetwork | true | URLSession spans |
flushOnBackground | true | |
debug | false | Print what the SDK does to the console |
Spans and logs are batched (every 5 seconds or 100 records) and written to the device first, so nothing is lost offline; the store is capped at 5 MB. If the key is rejected, the console says so once and sending stops for an hour. Zharp.flush() sends now.
Troubleshooting
Nothing arrives
On the app's setup page (App Telemetry → the app → settings → Setup instructions), the verify step shows the first event as soon as it lands, and Send a test event checks the key on its own. If the test event arrives but your app's don't, the SDK isn't starting or isn't reaching Zharp:
- Is
initcalled, and before the code you expect to see? Without a key, the SDK starts nothing and says so once. - Does the service name match? With an app key everything goes to that app whatever it says; with a workspace key,
servicedecides the app — see Keys. - Data is sent in batches every few seconds. A process that exits straight away should call
flushfirst. - Logs and errors arrive but no traces? Check that tracing isn't turned off — see Logs only.
- Building with Xcode 15 or older? The package needs Xcode 16.
- Set
options.debug = trueto print what the SDK does.
401 or 403
Ingest answers 401 when the key is missing, mistyped, revoked or rolled, and 403 when the request comes from an origin or IP address the key doesn't allow. Neither is retried: the SDK warns once and drops what was refused, and it never throws into your code. Copy the key again from the app's settings.