Skip to content

Logging & debugging

BGeo’s logging serves two different jobs. While you’re building your integration, it’s a local diagnostic tool — getLog() in a debug screen, Console.app, Xcode’s console. Once your app is in the field, it’s a server-first diagnostic tool: the same log lines your users generate on their own devices can be batched and uploaded to your backend, so you can see what happened on a device you’ll never physically hold. That second job is the differentiator — logging doesn’t wait for someone to reproduce a bug next to a debugger.

The native log store

Every engine event (motion-state changes, geofence transitions, upload outcomes, filter decisions, and more) and every line your own code writes through logger passes through the same native logger. Two things always happen, unconditionally:

  1. The line is mirrored to os_log — this happens regardless of any config, exactly like a normal platform log call.
  2. If logLevel admits the line’s level, it’s also persisted as a row in the on-device log table (log_entries in bgeo.db), where it becomes available to getLog() and eligible for upload via logUrl.

logLevel defaults to 0 (OFF) — nothing is persisted or eligible for upload out of the box, though the os_log mirror keeps working either way. Raise it to capture more:

logLevelConstantPersists
0logLevelOffNothing (default).
1logLevelErrorErrors only.
2logLevelWarningWarnings and above.
3logLevelInfoInformational and above.
4logLevelDebugDebug and above.
5logLevelVerboseEverything.

See the constants reference for the full constant list.

Each persisted row is a LogEntry: ts (ISO-8601 UTC), level (15), src ('native' or 'js' — the tag is carried over unchanged from the shared native store, so app-written rows still show up as 'js'), event (a short tag), and optional message/data.

Writing app logs

Your own code logs through the same pipe as the engine, via logger:

BackgroundGeolocation.logger.error("geofence sync failed", data: ["identifier": "home", "status": 500])
BackgroundGeolocation.logger.warn("sync deferred", data: ["pendingCount": 3, "reason": "offline"])
BackgroundGeolocation.logger.info("onboarding complete", data: ["step": "permissions"])
BackgroundGeolocation.logger.debug("rehydrated config from storage")
BackgroundGeolocation.logger.verbose("render tick", data: ["screen": "TrackMap"])

Each call is persisted with src: "js" (native engine lines are src: "native"), alongside whatever data map you pass. Logging never throws — every logger.* method returns Future<void> and swallows its own failures, so a logging call can never crash or reject into your app code.

Reading logs locally

getLog({limit}) returns persisted entries newest-first (default limit is 500):

let entries = await BackgroundGeolocation.getLog(limit: 200)
for entry in entries {
print("[\(entry.ts)] \(entry.src)/\(entry.event) \(entry.message ?? "")")
}

destroyLog() deletes every persisted row and resolves with the count removed — useful for clearing the table between test runs:

let removed = await BackgroundGeolocation.destroyLog()

Uploading logs

Set logUrl to have the native uploader batch persisted rows to your server. Without it, rows stay local-only and are only reachable through getLog().

try await BackgroundGeolocation.ready(
Config(
logUrl: "https://your-server.example/logs",
logLevel: LogLevel.info.rawValue,
logMaxDays: 3
)
)

Batches of up to 100 rows are posted as:

{
"events": [
{
"ts": "2026-07-24T11:24:03.512Z",
"level": "info",
"src": "native",
"event": "motionchange",
"message": "MOVING",
"data": { "confidence": 82 }
},
{
"ts": "2026-07-24T11:24:07.118Z",
"level": "warn",
"src": "js",
"event": "app",
"message": "sync deferred",
"data": { "pendingCount": 3, "reason": "offline" }
}
]
}

level is uploaded as its name ("error" | "warn" | "info" | "debug" | "verbose"), not the numeric LogEntry value. App-written lines always carry event: "app", with your message/data in message/data.

Log upload reuses the exact same headers, authorization, and JWT token-refresh machinery as location uploads — a killed-app log flush can refresh an expired access token just like a location flush can. There’s one important difference: onHttp fires only for location-sync requests — it does not observe log uploads at all.

Rows flush on:

  • a location-queue drain finishing (piggybacking on an already-warm connection),
  • the periodic heartbeat,
  • the app coming to the foreground,
  • 100 pending rows accumulating,
  • an explicit uploadLog() call, and
  • while the app is in the foreground only, a 3-second trailing-edge coalescing timer that arms on each logged line — so a line logged by an idle foreground app reaches your server in a few seconds instead of waiting for the next heartbeat. This timer never fires in the background, so logging never wakes the radio on its own.

A 2xx or 4xx response marks the batch uploaded (rows remain available to getLog() as local history until retention prunes them — an upload doesn’t delete anything). A 429 or 5xx/network failure leaves the batch pending for the next flush; 429 specifically means your server is throttling log ingestion (BGeo’s own backend limits this to 30 requests/minute/device) — it is not treated as a poison response the way a location 4xx is, so the batch is retried rather than dropped.

Retention: persisted rows are kept for logMaxDays (default 3), on top of a hard 25,000-row cap that prunes the oldest rows regardless of age.

uploadLog()

Trigger an out-of-band flush — for example, right after a user reports a problem, so you don’t wait for the next scheduled trigger:

let uploaded = await BackgroundGeolocation.uploadLog()

Resolves with the number of rows handed to the flusher, which isn’t necessarily the number successfully delivered — a 429/5xx batch stays queued for the next trigger.

Debug sound cues

Setting debug: true plays a one-shot audible cue on each of the following events, on both platforms. The players below are the exact files the SDK ships, so you can learn the set before you’re out in the field with a phone in your pocket. The use different recordings for the same event — learn the column for the platform you’re testing on.

EventCue
Location update — debug_location
Heartbeat — debug_heartbeat
Motion-change → moving — debug_motionchange_true
Motion-change → stationary — debug_motionchange_false
Stop-timeout armed — debug_stop_timeout_start
Stop-timeout cancelled (motion resumed) — debug_stop_timeout_cancel

Geofence ENTER / EXIT / DWELL reuses the location cue, so a transition sounds exactly like an ordinary location update.

This is a development convenience only — it never affects tracking, filtering, or upload behaviour. The cues can be inaudible depending on the device’s silent-switch/audio-session state; treat them as a “something happened” signal rather than a guaranteed audible alert.

diagnosticExtras

diagnosticExtras attaches a compact native diagnostic snapshot — fix counters, app and motion state, the active manager configuration — into every uploaded location record’s extras, rather than writing a log line.

It is meant for an instrumented test device while reproducing a field issue, not for a production fleet: it adds weight and noise to every location. It is also how the July 2026 background-delivery investigation was actually solved — per-point counter deltas showed CoreLocation delivering zero raw fixes during the gaps, which exonerated the filter and the upload queue in one reading.

Native-side inspection

Alongside getLog() and log upload, every line is still mirrored to the platform’s own log system, so you can watch it live from a connected device during development.

Xcode — the engine writes through os_log, so its lines appear in the console alongside your app’s own output while debugging.

Console.app — connect the device and filter by subsystem dev.bgeo. This is the one that matters for background work: it keeps receiving lines while the app is backgrounded, suspended, or relaunched by iOS, none of which a debugger session survives.

Web console

BGeo’s web console shows the device log stream live — once a device is uploading through logUrl, its incoming log rows appear in the console as they arrive, without you needing to pull them off the device yourself.

The web console's Logs tab streaming a linked device's log rows live, with the level filter row (All, Verbose, Debug, Info, Warn, Error) and the Follow toggle above it.

Troubleshooting checklist

Still stuck? File a report with a getLog() dump spanning the failure window alongside your ready() config (redact secrets) and device/OS details — see the support page’s “Before filing a bug” checklist for the full list.