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,
adb logcat, Console.app. 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 Dart code
writes through logger passes
through the same native logger. Two things always happen, unconditionally:
- The line is mirrored to
adb logcat(Android) oros_log(iOS) — this happens regardless of any config, exactly like a normal platform log call. - If
logLeveladmits the line’s level, it’s also persisted as a row in the on-device log table (log_entriesinbgeo.db), where it becomes available togetLog()and eligible for upload vialogUrl.
logLevel defaults to 0 (OFF) — nothing is persisted or eligible for
upload out of the box, though the logcat/os_log mirror keeps working either
way. Raise it to capture more:
logLevel | Constant | Persists |
|---|---|---|
0 | logLevelOff | Nothing (default). |
1 | logLevelError | Errors only. |
2 | logLevelWarning | Warnings and above. |
3 | logLevelInfo | Informational and above. |
4 | logLevelDebug | Debug and above. |
5 | logLevelVerbose | Everything. |
See the constants reference for the full constant list.
Each persisted row is a LogEntry:
ts (ISO-8601 UTC), level (1–5), src ('native' or 'js' — the
tag is carried over unchanged from the shared native store, so Dart-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', {'identifier': 'home', 'status': 500});BackgroundGeolocation.logger.warn('sync deferred', {'pendingCount': 3, 'reason': 'offline'});BackgroundGeolocation.logger.info('onboarding complete', {'step': 'permissions'});BackgroundGeolocation.logger.debug('rehydrated config from storage');BackgroundGeolocation.logger.verbose('render tick', {'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):
final entries = await BackgroundGeolocation.getLog(limit: 200);for (final entry in entries) { print('[${entry.ts}] ${entry.src}/${entry.event} ${entry.message} ${entry.data}');}destroyLog() deletes every
persisted row and resolves with the count removed — useful for clearing the
table between test runs:
final 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().
await BackgroundGeolocation.ready(Config( logLevel: logLevelInfo, logUrl: 'https://your-server.example/device/logs',));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:
final flushed = 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. iOS and Android
use different recordings for the same event — learn the column for the
platform you’re testing on.
| Event | iOS | Android |
|---|---|---|
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 behavior. On some iOS devices 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 is a
separate, narrower diagnostic: rather than a log line, it attaches a compact
native diagnostic snapshot (fix counters, app/motion state, active manager
configuration) into every uploaded location record’s extras. It’s
iOS-only — Android accepts the key but ignores it — and is intended for
instrumented test devices while reproducing a field issue, not for your
production fleet, since it adds payload weight and noise to every location.
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.
Android — filter logcat by the engine’s tag:
adb logcat -s BGGeoiOS — open Console.app, connect the device, and filter by subsystem
com.bgeo (categories are grouped by the event’s first dot-segment, e.g.
wake, motion, engine).
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.
Troubleshooting checklist
Still stuck? File a report with a getLog() dump spanning the
failure window (native + js sources) alongside your ready() config
(redact secrets) and device/OS details — see the support page’s “Before
filing a bug” checklist for the full list.