Diagnostics & troubleshooting
Background location is hard to debug because the interesting events happen when no one is watching — after the app is backgrounded, while it’s terminated, on someone else’s phone. This page covers the on-device log buffer, what Beekon throws vs. surfaces as a state, and a symptom → fix list.
The log surface
Section titled “The log surface”Beekon keeps an on-device log buffer you can read, export, and tail, so you can see what the SDK did while you weren’t looking. Logging is observational and self-contained: it never sends anything off the device.
| Method | What it does |
|---|---|
setLogLevel(level) | Set the verbosity threshold at runtime — entries below it aren’t recorded. |
log(level, message) | Write your own breadcrumb into the SDK’s pipeline (category app). |
getLog(from, to) | Read persisted entries in a time range, oldest first. |
exportLog() | Serialize the buffer to an NDJSON file; returns the file path (attach to a bug report). |
clearLog() | Delete all persisted entries. |
logs (stream) | Live tail — each entry as it’s recorded. |
A LogEntry is { id, timestamp, level, category, message } — category is the subsystem (location, sync, …), or app for breadcrumbs you write. Levels, most to least severe: error, warn, info, debug, verbose (plus off).
Beekon.setLogLevel(LogLevel.Debug)Beekon.log(LogLevel.Info, "user tapped 'start trip'")
lifecycleScope.launch { Beekon.logs.collect { e -> Log.d("beekon", "[${e.level}] ${e.category}: ${e.message}") }}
val entries = Beekon.getLog(from = hourAgo, to = Instant.now())val ndjsonPath = Beekon.exportLog()Beekon.shared.setLogLevel(.debug)Beekon.shared.log(.info, "user tapped 'start trip'")
Task { for await e in await Beekon.shared.logs { print("[\(e.level)] \(e.category): \(e.message)") }}
let entries = try await Beekon.shared.getLog(from: hourAgo, to: Date())let ndjsonPath = try await Beekon.shared.exportLog()await Beekon.instance.setLogLevel(LogLevel.debug);await Beekon.instance.log(LogLevel.info, "user tapped 'start trip'");
Beekon.instance.logs.listen((e) { print('[${e.level}] ${e.category}: ${e.message}');});
final entries = await Beekon.instance.getLog( from: DateTime.now().subtract(const Duration(hours: 1)), to: DateTime.now(),);final ndjsonPath = await Beekon.instance.exportLog();await Beekon.setLogLevel('debug');await Beekon.log('info', "user tapped 'start trip'");
Beekon.onLog((e) => console.log(`[${e.level}] ${e.category}: ${e.message}`));
const entries = await Beekon.getLog( new Date(Date.now() - 3_600_000), new Date(),);const ndjsonPath = await Beekon.exportLog();The error taxonomy
Section titled “The error taxonomy”Beekon errors are typed, but the SDK draws a hard line: start() never throws. Thrown errors are limited to storage failures (from history reads), geofence-validation failures (from addGeofences), location-unavailable failures (from getCurrentLocation on a precondition failure), and invalid-configuration failures (from configure). Everything else — permission, services, system kills — is a runtime condition that surfaces as Stopped(reason) on the state stream, not an exception.
The per-platform types:
- Android —
sealed class BeekonExceptionwith five subclasses:StorageException(cause),InvalidConfiguration(reason),InvalidGeofence(reason),GeofencesManagedByServer,LocationUnavailable(reason). - iOS —
enum BeekonError: Errorwith five cases:.invalidConfiguration(reason:),.invalidGeofence(reason:),.geofencesManagedByServer,.storage(reason:),.locationUnavailable(reason:).
Thrown vs. Stopped
Section titled “Thrown vs. Stopped”| Scenario | Surface |
|---|---|
User has never granted permission, you call start() | start() returns; state → Stopped(PermissionDenied) — no exception |
| User revokes permission while tracking | state → Stopped(PermissionDenied) — no exception |
| Android OS kills the foreground service under memory pressure | state → Stopped(System) — Android only |
getLocations(...) hits a SQLite failure | throws BeekonException.StorageException / BeekonError.storage |
addGeofences(...) given a bad region | throws BeekonException.InvalidGeofence / BeekonError.invalidGeofence |
configure(...) given an out-of-range value (e.g. stationary radius outside 5–1000 m) | throws BeekonException.InvalidConfiguration / BeekonError.invalidConfiguration |
There’s no NotInitialised or NotConfigured — calling start() without prior configure(...) simply uses the persisted last-known config (or the default on a fresh install). GeofencesManagedByServer fires only in Beekon Cloud mode.
Observe the state stream for everything to do with start/stop; reserve try/catch for history and geofence calls. Beekon does not auto-resume after a stop — the host app calls start() again once the underlying condition recovers.
Troubleshooting
Section titled “Troubleshooting”Symptom → likely cause → fix. Most issues are permissions, the gate, or host wiring — not the SDK.
start() seems to do nothing
Section titled “start() seems to do nothing”start() never throws — if it can’t run, the truth lands on state as Stopped(reason). Observe state and read the reason: permissionDenied, locationServicesDisabled, locationUnavailable, system. See Lifecycle & states.
Fixes are sparse or never arrive
Section titled “Fixes are sparse or never arrive”- The gate is too tight. A fix is admitted only when both
intervalSecondsanddistanceMeterspass. Lower them (or set to0to disable a gate). See Configuration. - No background permission. Foreground-only grants stop updates the moment the app backgrounds. You need Always / background location.
- iOS authorization is two-step. iOS grants When in Use first; you must then request Always. See Platform setup.
Sync keeps failing
Section titled “Sync keeps failing”- Wrong status code. The SDK only deletes rows on
2xx. A4xxyou didn’t mean (e.g. a framework returning404for an unmatched route) keeps the batch and looks like a stuck queue. Confirm your route returns2xx. - A redirect. Beekon refuses
3xxand treats it as permanent — serve the exacturldirectly, with no301/302. - You’re not gunzipping. The body is always
Content-Encoding: gzip. A server that hands raw bytes to a JSON parser will400. Gunzip first. - Auth. A
401/403surfaces as a sync auth failure. WithSyncConfig.authset, the SDK refreshes and retries; without it, re-seed by callingconfigure()again. See Auth & token refresh. - Watch
syncStatusandpendingUploadCount()to see whether the queue is draining.
Geofences don’t fire
Section titled “Geofences don’t fire”- Radius too small. Use ≥ 100 m — smaller regions are unreliable on real hardware.
- Background location not granted. Geofence monitoring needs the same Always/background grant as tracking.
- Notifications (Android 13+). If you also surface a notification on a crossing, you need
POST_NOTIFICATIONS. - Subscribe to
geofenceEventsto confirm crossings are being recorded. See Geofencing.
Tracking doesn’t resume after the app is killed
Section titled “Tracking doesn’t resume after the app is killed”- Android — wire the cold-launch resume. Call
Beekon.start()(orresumeIfNeeded()on the wrappers) fromApplication.onCreate. - iOS — it’s automatic, with a catch. A Significant Location Change wakes the terminated app and Beekon resumes. But if the user force-quits the app, iOS disables SLC wake until the app is next opened from the home screen — Apple’s rule, not Beekon’s.
- Flutter / React Native expose
resumeIfNeeded(); call it early in startup (and on Android, also fromApplication.onCreate).
Android: tracking stops on some devices
Section titled “Android: tracking stops on some devices”Aggressive OEM battery managers (Xiaomi, Oppo, Huawei, …) kill background services regardless of the foreground-service contract. There’s no SDK fix — guide users to exempt your app. dontkillmyapp.com has per-vendor steps.
React Native: module is undefined / build fails
Section titled “React Native: module is undefined / build fails”@wayq/beekon-rn requires the New Architecture (RN 0.76+). On the old architecture the TurboModule won’t link. Enable the New Architecture in your app.