fix(track-location): try 8
This commit is contained in:
parent
1980822919
commit
33eb0cfa13
5 changed files with 434 additions and 11 deletions
|
|
@ -15,7 +15,9 @@ Applies to the BackgroundGeolocation integration:
|
|||
## Current implementation notes
|
||||
|
||||
- Movement-driven recording only:
|
||||
- IDLE uses `geolocation.distanceFilter: 200` (aim: no updates while not moving).
|
||||
- IDLE relies on the SDK's **stop-detection + stationary geofence** (`geolocation.stopOnStationary` + `geolocation.stationaryRadius`) to avoid periodic stationary updates on Android.
|
||||
- When transitioning to moving (`onMotionChange(isMoving:true)`), JS requests **one** persisted fix (native `autoSync` uploads it).
|
||||
- We intentionally do not rely on time-based updates.
|
||||
- ACTIVE uses `geolocation.distanceFilter: 25`.
|
||||
- JS may request a persisted fix when entering ACTIVE (see [`applyProfile()`](src/location/trackLocation.js:351)).
|
||||
- Upload strategy is intentionally simple:
|
||||
|
|
@ -27,6 +29,8 @@ Applies to the BackgroundGeolocation integration:
|
|||
- Stationary noise suppression:
|
||||
- Native accuracy gate for persisted/uploaded locations: `geolocation.filter.trackingAccuracyThreshold: 100`.
|
||||
- Identical location suppression: `geolocation.allowIdenticalLocations: false`.
|
||||
- IDLE primarily relies on stop-detection + stationary geofence (`stopOnStationary: true`) to eliminate periodic stationary updates.
|
||||
- Elasticity disabled (`disableElasticity: true`) to avoid dynamic distanceFilter shrink.
|
||||
- Extra safety: any JS-triggered persisted fix requests are tagged and ignored if accuracy > 100m.
|
||||
|
||||
## Basic preconditions
|
||||
|
|
@ -43,7 +47,7 @@ Applies to the BackgroundGeolocation integration:
|
|||
2. Stay stationary for 5+ minutes.
|
||||
- Expect: no repeated server updates.
|
||||
3. Walk/drive ~250m.
|
||||
- Expect: at least one location persisted + uploaded.
|
||||
- Expect: `onMotionChange(isMoving:true)` then one persisted location + upload.
|
||||
|
||||
### ACTIVE (open alert)
|
||||
|
||||
|
|
@ -61,7 +65,7 @@ Applies to the BackgroundGeolocation integration:
|
|||
2. Stay stationary.
|
||||
- Expect: no periodic uploads.
|
||||
3. Move ~250m.
|
||||
- Expect: a persisted record and upload.
|
||||
- Expect: `onMotionChange(isMoving:true)` then one persisted record and upload.
|
||||
|
||||
### ACTIVE
|
||||
|
||||
|
|
@ -93,9 +97,9 @@ Applies to the BackgroundGeolocation integration:
|
|||
|
||||
| Platform | App state | Profile | Move | Expected signals |
|
||||
|---|---|---|---:|---|
|
||||
| Android | foreground | IDLE | ~250m | [`onLocation`](src/location/trackLocation.js:693) (sample=false), then [`onHttp`](src/location/trackLocation.js:733) |
|
||||
| Android | foreground | IDLE | ~250m | [`onMotionChange`](src/location/trackLocation.js:1192) then [`onLocation`](src/location/trackLocation.js:1085) (sample=false), then [`onHttp`](src/location/trackLocation.js:1150) |
|
||||
| Android | background | IDLE | ~250m | same as above |
|
||||
| Android | swipe-away | IDLE | ~250m | native persists + uploads; verify server + `onHttp` when app relaunches |
|
||||
| Android | swipe-away | IDLE | ~250m | native geofence triggers; verify server update; app may relaunch to deliver JS logs |
|
||||
| Android | foreground | ACTIVE | ~30m | location + upload continues |
|
||||
| iOS | background | IDLE | ~250m | movement-driven update; no periodic uploads while stationary |
|
||||
| iOS | swipe-killed | IDLE | significant | OS relaunch on movement; upload after relaunch |
|
||||
|
|
@ -105,7 +109,7 @@ Applies to the BackgroundGeolocation integration:
|
|||
- App lifecycle tagging: [`updateTrackingContextExtras()`](src/location/trackLocation.js:63) should update `tracking_ctx.app_state` on AppState changes.
|
||||
- No time-based uploads: heartbeat is disabled (`heartbeatInterval: 0`), so no `Heartbeat` logs from [`onHeartbeat`](src/location/trackLocation.js:762).
|
||||
- Movement-only uploads:
|
||||
- IDLE distance threshold: `distanceFilter: 200` in [`TRACKING_PROFILES`](src/location/backgroundGeolocationConfig.js:148).
|
||||
- IDLE: look for `Motion change` (isMoving=true) and `IDLE movement fallback fix`.
|
||||
- ACTIVE distance threshold: `distanceFilter: 25` in [`TRACKING_PROFILES`](src/location/backgroundGeolocationConfig.js:148).
|
||||
|
||||
- Attribution for `getCurrentPosition`:
|
||||
|
|
|
|||
27
plans/geolocation-periodic-uploads-investigation.md
Normal file
27
plans/geolocation-periodic-uploads-investigation.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Geolocation periodic uploads investigation
|
||||
|
||||
## Observations
|
||||
|
||||
From the app logs, we still see background IDLE uploads every ~5–10 minutes even when the device is stationary.
|
||||
|
||||
Key points:
|
||||
|
||||
- The upload is happening with `sample: undefined` (ie, persisted location) and is followed by HTTP success.
|
||||
- Motion state can flip to `isMovingState: true` even while `activity: still` and low `speed`.
|
||||
- In at least one diagnostic snapshot, `stopOnStationary` appeared as `undefined`, suggesting the config either:
|
||||
- is not being applied as expected, or
|
||||
- is being overridden by another config surface, or
|
||||
- is not exposed in `getState()` on that platform/version.
|
||||
|
||||
## Next steps
|
||||
|
||||
1. Confirm the currently running app build includes the latest config (`useSignificantChangesOnly`, `activity.stopOnStationary`).
|
||||
2. Extract precise server-side event timing distribution from Explore logs to see whether bursts correlate to motionchange or other triggers.
|
||||
3. If periodic uploads persist after significant-change mode:
|
||||
- evaluate whether another mechanism triggers periodic sync (eg native retry/reconnect),
|
||||
- consider disabling motion-activity updates in IDLE or increasing motion trigger delay,
|
||||
- optionally introduce a server-side dedupe (ignore updates if within X meters and within Y minutes) as last-resort safety.
|
||||
|
||||
## Notes
|
||||
|
||||
The Explore log snippet currently contains only `action` and `timestamp` (no payload coordinates), so it can confirm frequency/bursts but not whether the same point is repeatedly uploaded.
|
||||
|
|
@ -63,10 +63,21 @@ export const BASE_GEOLOCATION_CONFIG = {
|
|||
// even with no open alert (see TRACKING_PROFILES.idle).
|
||||
distanceFilter: 200,
|
||||
|
||||
// Prevent dynamic distanceFilter shrink.
|
||||
// Elasticity can lower the effective threshold (eg while stationary), resulting in
|
||||
// unexpected frequent updates.
|
||||
disableElasticity: true,
|
||||
|
||||
// Stop-detection.
|
||||
// NOTE: historically we set this at top-level. In v5 the knob is under `geolocation`.
|
||||
stopTimeout: 5,
|
||||
|
||||
// True-stationary strategy: once stop-detection decides we're stationary, stop active
|
||||
// tracking and rely on the stationary geofence to detect significant movement.
|
||||
// This is intended to eliminate periodic stationary updates on Android.
|
||||
stopOnStationary: true,
|
||||
stationaryRadius: 200,
|
||||
|
||||
// Prevent identical/noise locations from being persisted.
|
||||
// This reduces DB churn and avoids triggering native HTTP uploads with redundant points.
|
||||
allowIdenticalLocations: false,
|
||||
|
|
@ -170,8 +181,19 @@ export const TRACKING_PROFILES = {
|
|||
// Same rationale as BASE: prefer GPS-capable accuracy to avoid km-level coarse fixes
|
||||
// that can trigger false motion/geofence transitions on Android.
|
||||
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
|
||||
// Defensive: keep the distanceFilter conservative to avoid battery drain.
|
||||
|
||||
// IDLE runtime relies on the SDK's stop-detection + stationary geofence (stopOnStationary).
|
||||
// Keep geolocation config conservative for any incidental lookups.
|
||||
distanceFilter: 200,
|
||||
disableElasticity: true,
|
||||
|
||||
// IMPORTANT: ACTIVE sets stopOnStationary:false.
|
||||
// Ensure we restore it when transitioning back to IDLE, otherwise the SDK may
|
||||
// continue recording while stationary.
|
||||
stopOnStationary: true,
|
||||
|
||||
// QA helper: allow easier validation in dev/staging while keeping production at 200m.
|
||||
stationaryRadius: __DEV__ || env.IS_STAGING ? 30 : 200,
|
||||
|
||||
// Keep filtering enabled across profile transitions.
|
||||
filter: DEFAULT_LOCATION_FILTER,
|
||||
|
|
@ -183,8 +205,6 @@ export const TRACKING_PROFILES = {
|
|||
},
|
||||
activity: {
|
||||
// Android-only: reduce false-positive motion triggers due to screen-on/unlock.
|
||||
// We keep Motion API enabled (battery-optimized) but add a large delay so brief
|
||||
// activity-jitter cannot repeatedly toggle moving/stationary while the user is idle.
|
||||
// (This is ignored on iOS.)
|
||||
motionTriggerDelay: 300000,
|
||||
},
|
||||
|
|
@ -195,6 +215,12 @@ export const TRACKING_PROFILES = {
|
|||
// ACTIVE target: frequent updates while moving.
|
||||
distanceFilter: 25,
|
||||
|
||||
disableElasticity: true,
|
||||
|
||||
// While ACTIVE, do not stop updates simply because the device appears stationary.
|
||||
// Motion-detection + distanceFilter should govern updates.
|
||||
stopOnStationary: false,
|
||||
|
||||
// Apply the same native filter while ACTIVE.
|
||||
filter: DEFAULT_LOCATION_FILTER,
|
||||
allowIdenticalLocations: false,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export function getLastReadyState() {
|
|||
export function setBackgroundGeolocationEventHandlers({
|
||||
onLocation,
|
||||
onLocationError,
|
||||
onGeofence,
|
||||
onHttp,
|
||||
onHeartbeat,
|
||||
onSchedule,
|
||||
|
|
@ -66,6 +67,7 @@ export function setBackgroundGeolocationEventHandlers({
|
|||
// We use a simple signature so calling with identical functions is a no-op.
|
||||
const sig = [
|
||||
onLocation ? "L1" : "L0",
|
||||
onGeofence ? "G1" : "G0",
|
||||
onHttp ? "H1" : "H0",
|
||||
onHeartbeat ? "HB1" : "HB0",
|
||||
onSchedule ? "S1" : "S0",
|
||||
|
|
@ -87,6 +89,10 @@ export function setBackgroundGeolocationEventHandlers({
|
|||
BackgroundGeolocation.onLocation(onLocation, onLocationError),
|
||||
);
|
||||
}
|
||||
|
||||
if (onGeofence) {
|
||||
subscriptions.push(BackgroundGeolocation.onGeofence(onGeofence));
|
||||
}
|
||||
if (onHttp) {
|
||||
subscriptions.push(BackgroundGeolocation.onHttp(onHttp));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from "~/stores";
|
||||
|
||||
import setLocationState from "~/location/setLocationState";
|
||||
import { storeLocation } from "~/location/storage";
|
||||
import { getStoredLocation, storeLocation } from "~/location/storage";
|
||||
|
||||
import env from "~/env";
|
||||
|
||||
|
|
@ -32,6 +32,12 @@ import {
|
|||
|
||||
let trackLocationStartPromise = null;
|
||||
|
||||
// Correlation ID to differentiate multiple JS runtimes (eg full `Updates.reloadAsync()`)
|
||||
// from tree-level reloads (auth/account switch).
|
||||
const TRACK_LOCATION_INSTANCE_ID = `${Date.now().toString(36)}-${Math.random()
|
||||
.toString(16)
|
||||
.slice(2, 8)}`;
|
||||
|
||||
export default function trackLocation() {
|
||||
if (trackLocationStartPromise) return trackLocationStartPromise;
|
||||
|
||||
|
|
@ -41,6 +47,11 @@ export default function trackLocation() {
|
|||
feature: "tracking",
|
||||
});
|
||||
|
||||
locationLogger.info("trackLocation() starting", {
|
||||
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||
appState: AppState.currentState,
|
||||
});
|
||||
|
||||
let currentProfile = null;
|
||||
let authReady = false;
|
||||
let appState = AppState.currentState;
|
||||
|
|
@ -58,6 +69,160 @@ export default function trackLocation() {
|
|||
const BAD_ACCURACY_THRESHOLD_M = 200;
|
||||
const PERSISTED_ACCURACY_GATE_M = 100;
|
||||
|
||||
// NOTE: IDLE previously used `startGeofences()` + a managed geofence.
|
||||
// We now rely on the SDK's stop-detection + stationary geofence
|
||||
// (`stopOnStationary` + `stationaryRadius`) because it is more reliable
|
||||
// in background/locked scenarios.
|
||||
|
||||
// Fallback: if the OS fails to deliver geofence EXIT while the phone is locked, allow
|
||||
// exactly one persisted fix when we get strong evidence of movement (motion+activity).
|
||||
const IDLE_MOVEMENT_FALLBACK_COOLDOWN_MS = 15 * 60 * 1000;
|
||||
let lastActivity = null;
|
||||
let lastActivityConfidence = 0;
|
||||
let lastIdleMovementFallbackAt = 0;
|
||||
|
||||
// Diagnostics fields retained so server-side correlation can continue to work.
|
||||
// (With Option 2, these are used as a reference center rather than a managed geofence.)
|
||||
let lastEnsuredIdleGeofenceAt = 0;
|
||||
let lastIdleGeofenceCenter = null;
|
||||
let lastIdleGeofenceCenterAccuracyM = null;
|
||||
let lastIdleGeofenceCenterTimestamp = null;
|
||||
let lastIdleGeofenceCenterSource = null;
|
||||
let lastIdleGeofenceRadiusM = null;
|
||||
|
||||
// A) Safeguard: when entering IDLE, ensure we have a reasonably accurate and recent
|
||||
// reference point. This does NOT persist/upload; it only updates our stored last-known
|
||||
// location and tracking extras.
|
||||
const IDLE_REFERENCE_TARGET_ACCURACY_M = 50;
|
||||
const IDLE_REFERENCE_MAX_AGE_MS = 5 * 60 * 1000;
|
||||
const ensureIdleReferenceFix = async () => {
|
||||
try {
|
||||
const stored = await getStoredLocation();
|
||||
const storedCoords = stored?.coords;
|
||||
const storedAcc =
|
||||
typeof storedCoords?.accuracy === "number"
|
||||
? storedCoords.accuracy
|
||||
: null;
|
||||
const storedTs = stored?.timestamp;
|
||||
const storedAgeMs = storedTs
|
||||
? Date.now() - new Date(storedTs).getTime()
|
||||
: null;
|
||||
|
||||
const isRecentEnough =
|
||||
typeof storedAgeMs === "number" && storedAgeMs >= 0
|
||||
? storedAgeMs <= IDLE_REFERENCE_MAX_AGE_MS
|
||||
: false;
|
||||
const isAccurateEnough =
|
||||
typeof storedAcc === "number"
|
||||
? storedAcc <= IDLE_REFERENCE_TARGET_ACCURACY_M
|
||||
: false;
|
||||
|
||||
if (
|
||||
storedCoords?.latitude &&
|
||||
storedCoords?.longitude &&
|
||||
isRecentEnough &&
|
||||
isAccurateEnough
|
||||
) {
|
||||
lastIdleGeofenceCenter = {
|
||||
latitude: storedCoords.latitude,
|
||||
longitude: storedCoords.longitude,
|
||||
};
|
||||
lastIdleGeofenceCenterAccuracyM = storedAcc;
|
||||
lastIdleGeofenceCenterTimestamp = storedTs ?? null;
|
||||
lastIdleGeofenceCenterSource = "stored";
|
||||
lastEnsuredIdleGeofenceAt = Date.now();
|
||||
void updateTrackingContextExtras("idle_reference_ok");
|
||||
return;
|
||||
}
|
||||
|
||||
const fix = await getCurrentPositionWithDiagnostics(
|
||||
{
|
||||
samples: 2,
|
||||
timeout: 30,
|
||||
maximumAge: 0,
|
||||
desiredAccuracy: IDLE_REFERENCE_TARGET_ACCURACY_M,
|
||||
extras: {
|
||||
idle_reference_fix: true,
|
||||
idle_ref_prev_acc: storedAcc,
|
||||
idle_ref_prev_age_ms: storedAgeMs,
|
||||
},
|
||||
},
|
||||
{ reason: "idle_reference_fix", persist: false },
|
||||
);
|
||||
|
||||
if (fix?.coords?.latitude && fix?.coords?.longitude) {
|
||||
storeLocation(fix.coords, fix.timestamp);
|
||||
lastIdleGeofenceCenter = {
|
||||
latitude: fix.coords.latitude,
|
||||
longitude: fix.coords.longitude,
|
||||
};
|
||||
lastIdleGeofenceCenterAccuracyM =
|
||||
typeof fix.coords.accuracy === "number"
|
||||
? fix.coords.accuracy
|
||||
: null;
|
||||
lastIdleGeofenceCenterTimestamp = fix.timestamp ?? null;
|
||||
lastIdleGeofenceCenterSource = "idle_reference_fix";
|
||||
lastEnsuredIdleGeofenceAt = Date.now();
|
||||
void updateTrackingContextExtras("idle_reference_fixed");
|
||||
}
|
||||
} catch (e) {
|
||||
locationLogger.debug("Failed to ensure IDLE reference fix", {
|
||||
error: e?.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const maybeRequestIdleMovementFallbackFix = async (trigger) => {
|
||||
if (currentProfile !== "idle" || !authReady) return;
|
||||
if (
|
||||
Date.now() - lastIdleMovementFallbackAt <
|
||||
IDLE_MOVEMENT_FALLBACK_COOLDOWN_MS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Option 2: primary trigger is `onMotionChange(isMoving:true)`.
|
||||
// Keep `onActivityChange` as a secondary signal (lower confidence threshold).
|
||||
const movingActivities = new Set([
|
||||
"walking",
|
||||
"running",
|
||||
"on_foot",
|
||||
"in_vehicle",
|
||||
"cycling",
|
||||
]);
|
||||
const hasSomeActivitySignal =
|
||||
movingActivities.has(lastActivity) && lastActivityConfidence >= 50;
|
||||
|
||||
if (trigger === "activitychange" && !hasSomeActivitySignal) return;
|
||||
|
||||
lastIdleMovementFallbackAt = Date.now();
|
||||
locationLogger.info("IDLE movement fallback fix", {
|
||||
trigger,
|
||||
lastActivity,
|
||||
lastActivityConfidence,
|
||||
});
|
||||
|
||||
try {
|
||||
await getCurrentPositionWithDiagnostics(
|
||||
{
|
||||
samples: 2,
|
||||
timeout: 30,
|
||||
maximumAge: 0,
|
||||
desiredAccuracy: 50,
|
||||
extras: {
|
||||
idle_movement_fallback: true,
|
||||
},
|
||||
},
|
||||
{ reason: `idle_movement_fallback:${trigger}`, persist: true },
|
||||
);
|
||||
} catch (e) {
|
||||
locationLogger.warn("IDLE movement fallback fix failed", {
|
||||
trigger,
|
||||
error: e?.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const shouldUseLocationForUi = (location) => {
|
||||
const acc = location?.coords?.accuracy;
|
||||
return !(typeof acc === "number" && acc > BAD_ACCURACY_THRESHOLD_M);
|
||||
|
|
@ -115,6 +280,18 @@ export default function trackLocation() {
|
|||
profile: currentProfile,
|
||||
auth_ready: authReady,
|
||||
session_user_id: userId || null,
|
||||
// Diagnostics: helps correlate server-side "no update" reports with
|
||||
// the IDLE geofence placement parameters.
|
||||
idle_geofence: {
|
||||
// Option 2: managed IDLE geofence removed.
|
||||
id: null,
|
||||
radius_m: lastIdleGeofenceRadiusM,
|
||||
center: lastIdleGeofenceCenter,
|
||||
center_accuracy_m: lastIdleGeofenceCenterAccuracyM,
|
||||
center_timestamp: lastIdleGeofenceCenterTimestamp,
|
||||
center_source: lastIdleGeofenceCenterSource,
|
||||
ensured_at: lastEnsuredIdleGeofenceAt || null,
|
||||
},
|
||||
at: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
|
|
@ -433,10 +610,82 @@ export default function trackLocation() {
|
|||
// We only apply profile once auth headers are configured.
|
||||
return;
|
||||
}
|
||||
if (currentProfile === profileName) return;
|
||||
|
||||
// IMPORTANT:
|
||||
// Do not assume the native SDK runtime mode still matches our JS `currentProfile`.
|
||||
// During identity switch / tree reload, we can remain in the same logical profile
|
||||
// while native state drifts (eg `trackingMode` remains geofence-only but geofences
|
||||
// are missing, leading to "moving but no updates").
|
||||
//
|
||||
// If profile is unchanged, perform a lightweight runtime ensure:
|
||||
// - IDLE: ensure we're in geofence-only mode and that the managed exit geofence exists.
|
||||
// - ACTIVE: ensure we're NOT stuck in geofence-only mode.
|
||||
if (currentProfile === profileName) {
|
||||
try {
|
||||
const state = await BackgroundGeolocation.getState();
|
||||
|
||||
if (profileName === "idle") {
|
||||
// Ensure we are not stuck in geofence-only mode.
|
||||
if (state?.trackingMode === 0) {
|
||||
await BackgroundGeolocation.start();
|
||||
}
|
||||
locationLogger.info("Profile unchanged; IDLE runtime ensured", {
|
||||
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||
trackingMode: state?.trackingMode,
|
||||
enabled: state?.enabled,
|
||||
});
|
||||
void ensureIdleReferenceFix();
|
||||
}
|
||||
|
||||
if (profileName === "active") {
|
||||
// If we previously called `startGeofences()`, the SDK can remain in geofence-only
|
||||
// mode until we explicitly call `start()` again.
|
||||
if (state?.trackingMode === 0) {
|
||||
await BackgroundGeolocation.start();
|
||||
}
|
||||
locationLogger.info("Profile unchanged; ACTIVE runtime ensured", {
|
||||
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||
trackingMode: state?.trackingMode,
|
||||
enabled: state?.enabled,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
locationLogger.debug(
|
||||
"Failed to ensure runtime for unchanged profile",
|
||||
{
|
||||
profileName,
|
||||
error: e?.message,
|
||||
},
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const applyStartedAt = Date.now();
|
||||
|
||||
// Diagnostic: track trackingMode transitions (especially geofence-only mode) across
|
||||
// identity changes and profile switches.
|
||||
let preState = null;
|
||||
try {
|
||||
preState = await BackgroundGeolocation.getState();
|
||||
locationLogger.info("Applying tracking profile (pre-state)", {
|
||||
profileName,
|
||||
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||
enabled: preState?.enabled,
|
||||
isMoving: preState?.isMoving,
|
||||
trackingMode: preState?.trackingMode,
|
||||
distanceFilter: preState?.geolocation?.distanceFilter,
|
||||
});
|
||||
} catch (e) {
|
||||
locationLogger.debug(
|
||||
"Failed to read BGGeo state before profile apply",
|
||||
{
|
||||
profileName,
|
||||
error: e?.message,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const profile = TRACKING_PROFILES[profileName];
|
||||
if (!profile) {
|
||||
locationLogger.warn("Unknown tracking profile", { profileName });
|
||||
|
|
@ -462,6 +711,15 @@ export default function trackLocation() {
|
|||
// transitions so we still get distance-based updates when the user truly moves.
|
||||
if (profileName === "active") {
|
||||
const state = await BackgroundGeolocation.getState();
|
||||
|
||||
// If we were previously in geofence-only mode, switch back to standard tracking.
|
||||
// Without this, calling `changePace(true)` is not sufficient on some devices,
|
||||
// and the SDK can stay in `trackingMode: 0` (geofence-only), producing no
|
||||
// distance-based updates while moving.
|
||||
if (state?.trackingMode === 0) {
|
||||
await BackgroundGeolocation.start();
|
||||
}
|
||||
|
||||
if (!state?.isMoving) {
|
||||
await BackgroundGeolocation.changePace(true);
|
||||
}
|
||||
|
|
@ -519,6 +777,43 @@ export default function trackLocation() {
|
|||
// Update extras for observability (profile transitions are a key lifecycle change).
|
||||
updateTrackingContextExtras(`profile:${profileName}`);
|
||||
|
||||
// For IDLE, ensure we are NOT in geofence-only tracking mode.
|
||||
if (profileName === "idle") {
|
||||
try {
|
||||
const s = await BackgroundGeolocation.getState();
|
||||
if (s?.trackingMode === 0) {
|
||||
await BackgroundGeolocation.start();
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
void ensureIdleReferenceFix();
|
||||
}
|
||||
|
||||
// Post-state snapshot to detect if we're unintentionally left in geofence-only mode
|
||||
// after applying ACTIVE.
|
||||
try {
|
||||
const post = await BackgroundGeolocation.getState();
|
||||
locationLogger.info("Tracking profile applied (post-state)", {
|
||||
profileName,
|
||||
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||
enabled: post?.enabled,
|
||||
isMoving: post?.isMoving,
|
||||
trackingMode: post?.trackingMode,
|
||||
distanceFilter: post?.geolocation?.distanceFilter,
|
||||
// Comparing against preState helps debug transitions.
|
||||
prevTrackingMode: preState?.trackingMode ?? null,
|
||||
});
|
||||
} catch (e) {
|
||||
locationLogger.debug(
|
||||
"Failed to read BGGeo state after profile apply",
|
||||
{
|
||||
profileName,
|
||||
error: e?.message,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const state = await BackgroundGeolocation.getState();
|
||||
locationLogger.info("Tracking profile applied", {
|
||||
|
|
@ -557,8 +852,25 @@ export default function trackLocation() {
|
|||
|
||||
locationLogger.info("Handling auth token update", {
|
||||
hasToken: !!userToken,
|
||||
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||
});
|
||||
|
||||
// Snapshot state early so we can detect if the SDK is currently in geofence-only mode
|
||||
// when auth changes (common during IDLE profile).
|
||||
try {
|
||||
const s = await BackgroundGeolocation.getState();
|
||||
locationLogger.debug("Auth-change BGGeo state snapshot", {
|
||||
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||
enabled: s?.enabled,
|
||||
isMoving: s?.isMoving,
|
||||
trackingMode: s?.trackingMode,
|
||||
});
|
||||
} catch (e) {
|
||||
locationLogger.debug("Auth-change BGGeo state snapshot failed", {
|
||||
error: e?.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Compute identity from session store; this is our source of truth.
|
||||
// (A token refresh for the same user should not force a new persisted fix.)
|
||||
let currentSessionUserId = null;
|
||||
|
|
@ -802,7 +1114,32 @@ export default function trackLocation() {
|
|||
setLocationState(location.coords);
|
||||
// Also store in AsyncStorage for last known location fallback
|
||||
storeLocation(location.coords, location.timestamp);
|
||||
|
||||
// If we're IDLE, update reference center for later correlation.
|
||||
if (currentProfile === "idle") {
|
||||
lastIdleGeofenceCenter = {
|
||||
latitude: location.coords.latitude,
|
||||
longitude: location.coords.longitude,
|
||||
};
|
||||
lastIdleGeofenceCenterAccuracyM =
|
||||
typeof location.coords.accuracy === "number"
|
||||
? location.coords.accuracy
|
||||
: null;
|
||||
lastIdleGeofenceCenterTimestamp = location.timestamp ?? null;
|
||||
lastIdleGeofenceCenterSource = "onLocation";
|
||||
lastEnsuredIdleGeofenceAt = Date.now();
|
||||
void updateTrackingContextExtras("idle_reference_updated");
|
||||
}
|
||||
}
|
||||
},
|
||||
onGeofence: (event) => {
|
||||
// Minimal instrumentation to diagnose action semantics.
|
||||
locationLogger.info("Geofence event", {
|
||||
identifier: event?.identifier,
|
||||
action: event?.action,
|
||||
timestamp: event?.timestamp,
|
||||
hasGeofence: !!event?.geofence,
|
||||
});
|
||||
},
|
||||
onLocationError: (error) => {
|
||||
locationLogger.warn("Location error", {
|
||||
|
|
@ -881,8 +1218,18 @@ export default function trackLocation() {
|
|||
schedulerEnabled: state?.schedulerEnabled,
|
||||
// Critical config knobs related to periodic updates
|
||||
distanceFilter: state?.geolocation?.distanceFilter,
|
||||
disableElasticity: state?.geolocation?.disableElasticity,
|
||||
stationaryRadius: state?.geolocation?.stationaryRadius,
|
||||
stopOnStationary: state?.geolocation?.stopOnStationary,
|
||||
useSignificantChangesOnly:
|
||||
state?.geolocation?.useSignificantChangesOnly,
|
||||
allowIdenticalLocations:
|
||||
state?.geolocation?.allowIdenticalLocations,
|
||||
filter: state?.geolocation?.filter,
|
||||
heartbeatInterval: state?.app?.heartbeatInterval,
|
||||
motionTriggerDelay: state?.activity?.motionTriggerDelay,
|
||||
activityStopOnStationary: state?.activity?.stopOnStationary,
|
||||
disableStopDetection: state?.activity?.disableStopDetection,
|
||||
disableMotionActivityUpdates:
|
||||
state?.activity?.disableMotionActivityUpdates,
|
||||
stopTimeout: state?.geolocation?.stopTimeout,
|
||||
|
|
@ -948,12 +1295,25 @@ export default function trackLocation() {
|
|||
})();
|
||||
}
|
||||
}
|
||||
|
||||
// IDLE fallback: if we get a real motion transition while locked but geofence EXIT
|
||||
// is not delivered reliably, request one persisted fix (gated + cooled down).
|
||||
if (event?.isMoving && currentProfile === "idle" && authReady) {
|
||||
void maybeRequestIdleMovementFallbackFix("motionchange");
|
||||
}
|
||||
},
|
||||
onActivityChange: (event) => {
|
||||
locationLogger.info("Activity change", {
|
||||
activity: event?.activity,
|
||||
confidence: event?.confidence,
|
||||
});
|
||||
|
||||
lastActivity = event?.activity;
|
||||
lastActivityConfidence = event?.confidence ?? 0;
|
||||
|
||||
if (currentProfile === "idle" && authReady) {
|
||||
void maybeRequestIdleMovementFallbackFix("activitychange");
|
||||
}
|
||||
},
|
||||
onProviderChange: (event) => {
|
||||
locationLogger.info("Provider change", {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue