From 7ba78c7334462d6de6e647d236d39c415fa9cb2b Mon Sep 17 00:00:00 2001 From: devthejo Date: Fri, 23 Jan 2026 01:44:59 +0100 Subject: [PATCH] fix(track-location): try 8 --- docs/location-tracking-qa.md | 13 ++ src/location/backgroundGeolocationConfig.js | 27 +++ src/location/trackLocation.js | 198 ++++++++++++++------ 3 files changed, 185 insertions(+), 53 deletions(-) diff --git a/docs/location-tracking-qa.md b/docs/location-tracking-qa.md index 805cec7..523bfe6 100644 --- a/docs/location-tracking-qa.md +++ b/docs/location-tracking-qa.md @@ -24,6 +24,11 @@ Applies to the BackgroundGeolocation integration: - When authenticated, each persisted location should upload immediately via native HTTP (works while JS is suspended). - Pre-auth: tracking may persist locally but `http.url` is empty so nothing is uploaded until auth is ready. +- Stationary noise suppression: + - Native accuracy gate for persisted/uploaded locations: `geolocation.filter.trackingAccuracyThreshold: 100`. + - Identical location suppression: `geolocation.allowIdenticalLocations: false`. + - Extra safety: any JS-triggered persisted fix requests are tagged and ignored if accuracy > 100m. + ## Basic preconditions - Location permissions: foreground + background granted. @@ -103,6 +108,14 @@ Applies to the BackgroundGeolocation integration: - IDLE distance threshold: `distanceFilter: 200` in [`TRACKING_PROFILES`](src/location/backgroundGeolocationConfig.js:148). - ACTIVE distance threshold: `distanceFilter: 25` in [`TRACKING_PROFILES`](src/location/backgroundGeolocationConfig.js:148). +- Attribution for `getCurrentPosition`: + - `Location update received` logs include `extras.req_reason` and `extras.req_persist`. + - Persisted-fix reasons to expect: `active_profile_enter`, `moving_edge`, `startup_fix`, `identity_fix:*`. + +- Accuracy gate signals: + - A persisted-fix request can be logged but later ignored due to accuracy > 100m. + - If the server still receives periodic updates while stationary, check that the uploaded record has acceptable accuracy and that the device isn't flapping between moving/stationary. + ## Debugging tips - Observe logs in app: diff --git a/src/location/backgroundGeolocationConfig.js b/src/location/backgroundGeolocationConfig.js index 648a0c3..7b15136 100644 --- a/src/location/backgroundGeolocationConfig.js +++ b/src/location/backgroundGeolocationConfig.js @@ -1,6 +1,18 @@ import BackgroundGeolocation from "react-native-background-geolocation"; import env from "~/env"; +const LOCATION_ACCURACY_GATE_M = 100; + +// Native filter to reduce GPS drift and suppress stationary jitter. +// This is the primary mechanism to prevent unwanted persisted/uploaded points while the device +// is not moving (works even when JS is suspended). +const DEFAULT_LOCATION_FILTER = { + policy: BackgroundGeolocation.LocationFilterPolicy.Conservative, + useKalman: true, + kalmanProfile: BackgroundGeolocation.KalmanProfile.Conservative, + trackingAccuracyThreshold: LOCATION_ACCURACY_GATE_M, +}; + // Common config: keep always-on tracking enabled, but default to an IDLE low-power profile. // High-accuracy and tighter distance thresholds are enabled only when an active alert is open. // @@ -55,6 +67,13 @@ export const BASE_GEOLOCATION_CONFIG = { // NOTE: historically we set this at top-level. In v5 the knob is under `geolocation`. stopTimeout: 5, + // Prevent identical/noise locations from being persisted. + // This reduces DB churn and avoids triggering native HTTP uploads with redundant points. + allowIdenticalLocations: false, + + // Native-side filter (see DEFAULT_LOCATION_FILTER above). + filter: DEFAULT_LOCATION_FILTER, + // Permission request strategy locationAuthorizationRequest: "Always", }, @@ -153,6 +172,10 @@ export const TRACKING_PROFILES = { desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High, // Defensive: keep the distanceFilter conservative to avoid battery drain. distanceFilter: 200, + + // Keep filtering enabled across profile transitions. + filter: DEFAULT_LOCATION_FILTER, + allowIdenticalLocations: false, }, app: { // Never use heartbeat-driven updates; only movement-driven. @@ -171,6 +194,10 @@ export const TRACKING_PROFILES = { desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High, // ACTIVE target: frequent updates while moving. distanceFilter: 25, + + // Apply the same native filter while ACTIVE. + filter: DEFAULT_LOCATION_FILTER, + allowIdenticalLocations: false, }, app: { // Never use heartbeat-driven updates; only movement-driven. diff --git a/src/location/trackLocation.js b/src/location/trackLocation.js index 64a3774..1fbf381 100644 --- a/src/location/trackLocation.js +++ b/src/location/trackLocation.js @@ -56,6 +56,49 @@ export default function trackLocation() { const MOVING_EDGE_COOLDOWN_MS = 5 * 60 * 1000; const BAD_ACCURACY_THRESHOLD_M = 200; + const PERSISTED_ACCURACY_GATE_M = 100; + + const shouldUseLocationForUi = (location) => { + const acc = location?.coords?.accuracy; + return !(typeof acc === "number" && acc > BAD_ACCURACY_THRESHOLD_M); + }; + + // Gate persisted/uploaded points (native layer also filters, but this protects any + // JS-triggered persisted-fix code-paths). + const shouldAllowPersistedFix = (location) => { + const acc = location?.coords?.accuracy; + return !(typeof acc === "number" && acc > PERSISTED_ACCURACY_GATE_M); + }; + + const getCurrentPositionWithDiagnostics = async ( + options, + { reason, persist }, + ) => { + const opts = { + ...options, + persist, + extras: { + ...(options?.extras || {}), + // Attribution for log correlation. + req_reason: reason, + req_persist: !!persist, + req_at: new Date().toISOString(), + req_app_state: appState, + req_profile: currentProfile, + }, + }; + + locationLogger.debug("Requesting getCurrentPosition", { + reason, + persist: !!persist, + desiredAccuracy: opts?.desiredAccuracy, + samples: opts?.samples, + maximumAge: opts?.maximumAge, + timeout: opts?.timeout, + }); + + return BackgroundGeolocation.getCurrentPosition(opts); + }; // Track identity so we can force a first geopoint when the effective user changes. let lastSessionUserId = null; @@ -145,18 +188,31 @@ export default function trackLocation() { const requestIdentityPersistedFixAndSync = async ({ reason, userId }) => { try { const t0 = Date.now(); - const location = await BackgroundGeolocation.getCurrentPosition({ - samples: 1, - persist: true, - timeout: 30, - maximumAge: 0, - desiredAccuracy: 50, - extras: { - identity_fix: true, - identity_reason: reason, - session_user_id: userId, + const location = await getCurrentPositionWithDiagnostics( + { + samples: 1, + timeout: 30, + maximumAge: 0, + desiredAccuracy: 50, + extras: { + identity_fix: true, + identity_reason: reason, + session_user_id: userId, + }, }, - }); + { reason: `identity_fix:${reason}`, persist: true }, + ); + + if (!shouldAllowPersistedFix(location)) { + locationLogger.info( + "Identity persisted fix ignored due to poor accuracy", + { + reason, + userId, + accuracy: location?.coords?.accuracy, + }, + ); + } locationLogger.info("Identity persisted fix acquired", { reason, userId, @@ -206,16 +262,29 @@ export default function trackLocation() { } const t0 = Date.now(); - const location = await BackgroundGeolocation.getCurrentPosition({ - samples: 1, - persist: true, - timeout: 30, - maximumAge: 10000, - desiredAccuracy: 100, - extras: { - startup_fix: true, + const location = await getCurrentPositionWithDiagnostics( + { + samples: 1, + timeout: 30, + maximumAge: 10000, + desiredAccuracy: 100, + extras: { + startup_fix: true, + }, }, - }); + { reason: "startup_fix", persist: true }, + ); + + if (!shouldAllowPersistedFix(location)) { + locationLogger.info( + "Startup persisted fix ignored due to poor accuracy", + { + accuracy: location?.coords?.accuracy, + timestamp: location?.timestamp, + }, + ); + return; + } locationLogger.info("Startup persisted fix acquired", { ms: Date.now() - t0, @@ -290,19 +359,18 @@ export default function trackLocation() { return; } - const location = await BackgroundGeolocation.getCurrentPosition({ - samples: 1, - // IMPORTANT: do not persist by default. - // Persisting will create a DB record and the SDK may upload it on resume, - // which is the source of "updates while not moved" on some devices. - persist: false, - timeout: 20, - maximumAge: 10000, - desiredAccuracy: 100, - extras: { - auth_token_update: true, + const location = await getCurrentPositionWithDiagnostics( + { + samples: 1, + timeout: 20, + maximumAge: 10000, + desiredAccuracy: 100, + extras: { + auth_token_update: true, + }, }, - }); + { reason: "auth_change_ui_fix", persist: false }, + ); // If the fix is very poor accuracy, treat it as noise and do nothing. // (We intentionally do not persist in this path.) @@ -403,16 +471,28 @@ export default function trackLocation() { // motion-detection / distanceFilter to produce the first point. try { const beforeFix = Date.now(); - const fix = await BackgroundGeolocation.getCurrentPosition({ - samples: 3, - persist: true, - timeout: 30, - maximumAge: 0, - desiredAccuracy: 10, - extras: { - active_profile_enter: true, + const fix = await getCurrentPositionWithDiagnostics( + { + samples: 3, + timeout: 30, + maximumAge: 0, + desiredAccuracy: 10, + extras: { + active_profile_enter: true, + }, }, - }); + { reason: "active_profile_enter", persist: true }, + ); + + if (!shouldAllowPersistedFix(fix)) { + locationLogger.info( + "ACTIVE immediate persisted fix ignored due to poor accuracy", + { + accuracy: fix?.coords?.accuracy, + }, + ); + return; + } locationLogger.info("ACTIVE immediate fix acquired", { ms: Date.now() - beforeFix, accuracy: fix?.coords?.accuracy, @@ -697,6 +777,7 @@ export default function trackLocation() { activity: location.activity, battery: location.battery, sample: location.sample, + extras: location.extras, }); // Ignore sampling locations (eg, emitted during getCurrentPosition) to avoid UI/storage churn. @@ -705,10 +786,9 @@ export default function trackLocation() { // Quality gate (UI-only): if accuracy is very poor, ignore for UI/state. // Do NOT delete the record here; native uploads may happen while JS is suspended. - const acc = location?.coords?.accuracy; - if (typeof acc === "number" && acc > BAD_ACCURACY_THRESHOLD_M) { + if (!shouldUseLocationForUi(location)) { locationLogger.info("Ignoring poor-accuracy location", { - accuracy: acc, + accuracy: location?.coords?.accuracy, uuid: location?.uuid, }); return; @@ -830,16 +910,28 @@ export default function trackLocation() { lastMovingEdgeAt = now; (async () => { try { - const fix = await BackgroundGeolocation.getCurrentPosition({ - samples: 1, - persist: true, - timeout: 30, - maximumAge: 0, - desiredAccuracy: 50, - extras: { - moving_edge: true, + const fix = await getCurrentPositionWithDiagnostics( + { + samples: 1, + timeout: 30, + maximumAge: 0, + desiredAccuracy: 50, + extras: { + moving_edge: true, + }, }, - }); + { reason: "moving_edge", persist: true }, + ); + + if (!shouldAllowPersistedFix(fix)) { + locationLogger.info( + "Moving-edge persisted fix ignored due to poor accuracy", + { + accuracy: fix?.coords?.accuracy, + }, + ); + return; + } locationLogger.info("Moving-edge fix acquired", { accuracy: fix?.coords?.accuracy, latitude: fix?.coords?.latitude,