fix(track-location): try 8
This commit is contained in:
parent
f4f7708e71
commit
7ba78c7334
3 changed files with 185 additions and 53 deletions
|
|
@ -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).
|
- 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.
|
- 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
|
## Basic preconditions
|
||||||
|
|
||||||
- Location permissions: foreground + background granted.
|
- 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).
|
- 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).
|
- 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
|
## Debugging tips
|
||||||
|
|
||||||
- Observe logs in app:
|
- Observe logs in app:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
import BackgroundGeolocation from "react-native-background-geolocation";
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
import env from "~/env";
|
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.
|
// 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.
|
// 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`.
|
// NOTE: historically we set this at top-level. In v5 the knob is under `geolocation`.
|
||||||
stopTimeout: 5,
|
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
|
// Permission request strategy
|
||||||
locationAuthorizationRequest: "Always",
|
locationAuthorizationRequest: "Always",
|
||||||
},
|
},
|
||||||
|
|
@ -153,6 +172,10 @@ export const TRACKING_PROFILES = {
|
||||||
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
|
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
|
||||||
// Defensive: keep the distanceFilter conservative to avoid battery drain.
|
// Defensive: keep the distanceFilter conservative to avoid battery drain.
|
||||||
distanceFilter: 200,
|
distanceFilter: 200,
|
||||||
|
|
||||||
|
// Keep filtering enabled across profile transitions.
|
||||||
|
filter: DEFAULT_LOCATION_FILTER,
|
||||||
|
allowIdenticalLocations: false,
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
// Never use heartbeat-driven updates; only movement-driven.
|
// Never use heartbeat-driven updates; only movement-driven.
|
||||||
|
|
@ -171,6 +194,10 @@ export const TRACKING_PROFILES = {
|
||||||
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
|
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
|
||||||
// ACTIVE target: frequent updates while moving.
|
// ACTIVE target: frequent updates while moving.
|
||||||
distanceFilter: 25,
|
distanceFilter: 25,
|
||||||
|
|
||||||
|
// Apply the same native filter while ACTIVE.
|
||||||
|
filter: DEFAULT_LOCATION_FILTER,
|
||||||
|
allowIdenticalLocations: false,
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
// Never use heartbeat-driven updates; only movement-driven.
|
// Never use heartbeat-driven updates; only movement-driven.
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,49 @@ export default function trackLocation() {
|
||||||
const MOVING_EDGE_COOLDOWN_MS = 5 * 60 * 1000;
|
const MOVING_EDGE_COOLDOWN_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
const BAD_ACCURACY_THRESHOLD_M = 200;
|
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.
|
// Track identity so we can force a first geopoint when the effective user changes.
|
||||||
let lastSessionUserId = null;
|
let lastSessionUserId = null;
|
||||||
|
|
@ -145,9 +188,9 @@ export default function trackLocation() {
|
||||||
const requestIdentityPersistedFixAndSync = async ({ reason, userId }) => {
|
const requestIdentityPersistedFixAndSync = async ({ reason, userId }) => {
|
||||||
try {
|
try {
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
const location = await BackgroundGeolocation.getCurrentPosition({
|
const location = await getCurrentPositionWithDiagnostics(
|
||||||
|
{
|
||||||
samples: 1,
|
samples: 1,
|
||||||
persist: true,
|
|
||||||
timeout: 30,
|
timeout: 30,
|
||||||
maximumAge: 0,
|
maximumAge: 0,
|
||||||
desiredAccuracy: 50,
|
desiredAccuracy: 50,
|
||||||
|
|
@ -156,7 +199,20 @@ export default function trackLocation() {
|
||||||
identity_reason: reason,
|
identity_reason: reason,
|
||||||
session_user_id: userId,
|
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", {
|
locationLogger.info("Identity persisted fix acquired", {
|
||||||
reason,
|
reason,
|
||||||
userId,
|
userId,
|
||||||
|
|
@ -206,16 +262,29 @@ export default function trackLocation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
const location = await BackgroundGeolocation.getCurrentPosition({
|
const location = await getCurrentPositionWithDiagnostics(
|
||||||
|
{
|
||||||
samples: 1,
|
samples: 1,
|
||||||
persist: true,
|
|
||||||
timeout: 30,
|
timeout: 30,
|
||||||
maximumAge: 10000,
|
maximumAge: 10000,
|
||||||
desiredAccuracy: 100,
|
desiredAccuracy: 100,
|
||||||
extras: {
|
extras: {
|
||||||
startup_fix: true,
|
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", {
|
locationLogger.info("Startup persisted fix acquired", {
|
||||||
ms: Date.now() - t0,
|
ms: Date.now() - t0,
|
||||||
|
|
@ -290,19 +359,18 @@ export default function trackLocation() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const location = await BackgroundGeolocation.getCurrentPosition({
|
const location = await getCurrentPositionWithDiagnostics(
|
||||||
|
{
|
||||||
samples: 1,
|
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,
|
timeout: 20,
|
||||||
maximumAge: 10000,
|
maximumAge: 10000,
|
||||||
desiredAccuracy: 100,
|
desiredAccuracy: 100,
|
||||||
extras: {
|
extras: {
|
||||||
auth_token_update: true,
|
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.
|
// If the fix is very poor accuracy, treat it as noise and do nothing.
|
||||||
// (We intentionally do not persist in this path.)
|
// (We intentionally do not persist in this path.)
|
||||||
|
|
@ -403,16 +471,28 @@ export default function trackLocation() {
|
||||||
// motion-detection / distanceFilter to produce the first point.
|
// motion-detection / distanceFilter to produce the first point.
|
||||||
try {
|
try {
|
||||||
const beforeFix = Date.now();
|
const beforeFix = Date.now();
|
||||||
const fix = await BackgroundGeolocation.getCurrentPosition({
|
const fix = await getCurrentPositionWithDiagnostics(
|
||||||
|
{
|
||||||
samples: 3,
|
samples: 3,
|
||||||
persist: true,
|
|
||||||
timeout: 30,
|
timeout: 30,
|
||||||
maximumAge: 0,
|
maximumAge: 0,
|
||||||
desiredAccuracy: 10,
|
desiredAccuracy: 10,
|
||||||
extras: {
|
extras: {
|
||||||
active_profile_enter: true,
|
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", {
|
locationLogger.info("ACTIVE immediate fix acquired", {
|
||||||
ms: Date.now() - beforeFix,
|
ms: Date.now() - beforeFix,
|
||||||
accuracy: fix?.coords?.accuracy,
|
accuracy: fix?.coords?.accuracy,
|
||||||
|
|
@ -697,6 +777,7 @@ export default function trackLocation() {
|
||||||
activity: location.activity,
|
activity: location.activity,
|
||||||
battery: location.battery,
|
battery: location.battery,
|
||||||
sample: location.sample,
|
sample: location.sample,
|
||||||
|
extras: location.extras,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ignore sampling locations (eg, emitted during getCurrentPosition) to avoid UI/storage churn.
|
// 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.
|
// 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.
|
// Do NOT delete the record here; native uploads may happen while JS is suspended.
|
||||||
const acc = location?.coords?.accuracy;
|
if (!shouldUseLocationForUi(location)) {
|
||||||
if (typeof acc === "number" && acc > BAD_ACCURACY_THRESHOLD_M) {
|
|
||||||
locationLogger.info("Ignoring poor-accuracy location", {
|
locationLogger.info("Ignoring poor-accuracy location", {
|
||||||
accuracy: acc,
|
accuracy: location?.coords?.accuracy,
|
||||||
uuid: location?.uuid,
|
uuid: location?.uuid,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -830,16 +910,28 @@ export default function trackLocation() {
|
||||||
lastMovingEdgeAt = now;
|
lastMovingEdgeAt = now;
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const fix = await BackgroundGeolocation.getCurrentPosition({
|
const fix = await getCurrentPositionWithDiagnostics(
|
||||||
|
{
|
||||||
samples: 1,
|
samples: 1,
|
||||||
persist: true,
|
|
||||||
timeout: 30,
|
timeout: 30,
|
||||||
maximumAge: 0,
|
maximumAge: 0,
|
||||||
desiredAccuracy: 50,
|
desiredAccuracy: 50,
|
||||||
extras: {
|
extras: {
|
||||||
moving_edge: true,
|
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", {
|
locationLogger.info("Moving-edge fix acquired", {
|
||||||
accuracy: fix?.coords?.accuracy,
|
accuracy: fix?.coords?.accuracy,
|
||||||
latitude: fix?.coords?.latitude,
|
latitude: fix?.coords?.latitude,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue