boltZharp Docs
phone_iphoneSDKs

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.

info
Zharp for iOS 0.1.0

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.
Run Script
# 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"
fi

ZHARP_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:

Run Script
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
fi

For 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 it
info

Spans 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.DefaultNotes
key—Required. The app key, zpk_….
serviceThe bundle identifier
environmentnone
release"2.3.0 (412)"The version and build from the bundle
endpointhttps://api.zharp.io/api/v1/otlp
tracingtruefalse is logs only
tracingOrigins[]Hosts that get traceparent and baggage
sampleRate1.0Share of traces kept; errors, crashes and logs are always sent
captureCrashestrue
detectHangs / hangThresholdtrue / 2 s
trackScreenstrueUIKit view controllers
traceNetworktrueURLSession spans
flushOnBackgroundtrue
debugfalsePrint 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 init called, 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, service decides the app — see Keys.
  • Data is sent in batches every few seconds. A process that exits straight away should call flush first.
  • 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 = true to 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.