fix(track-location): try 3
This commit is contained in:
parent
8e81b1fa73
commit
41bb6fcd2d
2 changed files with 268 additions and 14 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import BackgroundGeolocation from "react-native-background-geolocation";
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
|
import { Platform } from "react-native";
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
@ -112,12 +113,19 @@ export const TRACKING_PROFILES = {
|
||||||
// only periodic fixes (several times/hour). Note many config options like
|
// only periodic fixes (several times/hour). Note many config options like
|
||||||
// `distanceFilter` / `stationaryRadius` are documented as having little/no
|
// `distanceFilter` / `stationaryRadius` are documented as having little/no
|
||||||
// effect in this mode.
|
// effect in this mode.
|
||||||
useSignificantChangesOnly: true,
|
// Some iOS devices / user settings can result in unreliable significant-change wakeups.
|
||||||
|
// We keep SLC for Android (battery), but fall back to standard motion tracking on iOS
|
||||||
|
// with a conservative distanceFilter.
|
||||||
|
useSignificantChangesOnly: Platform.OS !== "ios",
|
||||||
|
|
||||||
// Defensive: if some devices/platform conditions fall back to standard tracking,
|
// Defensive: if some devices/platform conditions fall back to standard tracking,
|
||||||
// keep the distanceFilter conservative to avoid battery drain.
|
// keep the distanceFilter conservative to avoid battery drain.
|
||||||
distanceFilter: 200,
|
distanceFilter: 200,
|
||||||
|
|
||||||
|
// Android-only: reduce false-positive motion triggers due to screen-on/unlock.
|
||||||
|
// (This is ignored on iOS.)
|
||||||
|
motionTriggerDelay: 30000,
|
||||||
|
|
||||||
// Keep the default stop-detection timing (minutes). In significant-changes
|
// Keep the default stop-detection timing (minutes). In significant-changes
|
||||||
// mode, stop-detection is not the primary driver of updates.
|
// mode, stop-detection is not the primary driver of updates.
|
||||||
stopTimeout: 5,
|
stopTimeout: 5,
|
||||||
|
|
@ -132,6 +140,9 @@ export const TRACKING_PROFILES = {
|
||||||
distanceFilter: 50,
|
distanceFilter: 50,
|
||||||
heartbeatInterval: 60,
|
heartbeatInterval: 60,
|
||||||
|
|
||||||
|
// Android-only: do not delay motion triggers while ACTIVE.
|
||||||
|
motionTriggerDelay: 0,
|
||||||
|
|
||||||
// Keep default responsiveness during an active alert.
|
// Keep default responsiveness during an active alert.
|
||||||
stopTimeout: 5,
|
stopTimeout: 5,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -45,19 +45,169 @@ export default function trackLocation() {
|
||||||
let stopAlertSubscription = null;
|
let stopAlertSubscription = null;
|
||||||
let stopSessionSubscription = null;
|
let stopSessionSubscription = null;
|
||||||
|
|
||||||
|
// Pre-login behavior: keep BGGeo running (so we can collect a first point), but disable
|
||||||
|
// uploads until we have an auth token.
|
||||||
|
let didDisableUploadsForAnonymous = false;
|
||||||
|
let didSyncAfterAuth = false;
|
||||||
|
let didSyncAfterStartupFix = false;
|
||||||
|
|
||||||
|
// Track identity so we can force a first geopoint when the effective user changes.
|
||||||
|
let lastSessionUserId = null;
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
const safeSync = async (reason) => {
|
||||||
|
// Sync can fail transiently (SDK busy, network warming up, etc). Retry a few times.
|
||||||
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||||
|
try {
|
||||||
|
const [state, pendingBefore] = await Promise.all([
|
||||||
|
BackgroundGeolocation.getState(),
|
||||||
|
BackgroundGeolocation.getCount(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
locationLogger.info("Attempting BGGeo sync", {
|
||||||
|
reason,
|
||||||
|
attempt,
|
||||||
|
enabled: state?.enabled,
|
||||||
|
isMoving: state?.isMoving,
|
||||||
|
trackingMode: state?.trackingMode,
|
||||||
|
pendingBefore,
|
||||||
|
});
|
||||||
|
|
||||||
|
const records = await BackgroundGeolocation.sync();
|
||||||
|
const pendingAfter = await BackgroundGeolocation.getCount();
|
||||||
|
|
||||||
|
locationLogger.info("BGGeo sync success", {
|
||||||
|
reason,
|
||||||
|
attempt,
|
||||||
|
synced: records?.length,
|
||||||
|
pendingAfter,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
const msg =
|
||||||
|
typeof e === "string"
|
||||||
|
? e
|
||||||
|
: e?.message || e?.error || JSON.stringify(e);
|
||||||
|
locationLogger.warn("BGGeo sync failed", {
|
||||||
|
reason,
|
||||||
|
attempt,
|
||||||
|
error: msg,
|
||||||
|
stack: e?.stack,
|
||||||
|
});
|
||||||
|
await sleep(attempt * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
locationLogger.info("Identity persisted fix acquired", {
|
||||||
|
reason,
|
||||||
|
userId,
|
||||||
|
ms: Date.now() - t0,
|
||||||
|
accuracy: location?.coords?.accuracy,
|
||||||
|
latitude: location?.coords?.latitude,
|
||||||
|
longitude: location?.coords?.longitude,
|
||||||
|
timestamp: location?.timestamp,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.warn("Identity persisted fix failed", {
|
||||||
|
reason,
|
||||||
|
userId,
|
||||||
|
error: e?.message,
|
||||||
|
stack: e?.stack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await safeSync(`identity-fix:${reason}`);
|
||||||
|
};
|
||||||
|
|
||||||
// One-off startup refresh: when tracking is enabled at app launch, fetch a fresh fix once.
|
// One-off startup refresh: when tracking is enabled at app launch, fetch a fresh fix once.
|
||||||
// This follows Transistorsoft docs guidance to use getCurrentPosition rather than forcing
|
// This follows Transistorsoft docs guidance to use getCurrentPosition rather than forcing
|
||||||
// the SDK into moving mode with changePace(true).
|
// the SDK into moving mode with changePace(true).
|
||||||
let didRequestStartupFix = false;
|
let didRequestStartupFix = false;
|
||||||
let startupFixInFlight = null;
|
let startupFixInFlight = null;
|
||||||
|
|
||||||
|
// Startup fix should be persisted so it can be auto-synced immediately (user expects
|
||||||
|
// to appear on server soon after first app open). This is intentionally different
|
||||||
|
// from auth-refresh fixes, which are non-persisted to avoid unlock/resume noise.
|
||||||
|
const requestStartupPersistedFix = async () => {
|
||||||
|
try {
|
||||||
|
const before = await BackgroundGeolocation.getState();
|
||||||
|
locationLogger.info("Requesting startup persisted location fix", {
|
||||||
|
enabled: before.enabled,
|
||||||
|
trackingMode: before.trackingMode,
|
||||||
|
isMoving: before.isMoving,
|
||||||
|
});
|
||||||
|
|
||||||
|
const t0 = Date.now();
|
||||||
|
const location = await BackgroundGeolocation.getCurrentPosition({
|
||||||
|
samples: 1,
|
||||||
|
persist: true,
|
||||||
|
timeout: 30,
|
||||||
|
maximumAge: 10000,
|
||||||
|
desiredAccuracy: 100,
|
||||||
|
extras: {
|
||||||
|
startup_fix: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
locationLogger.info("Startup persisted fix acquired", {
|
||||||
|
ms: Date.now() - t0,
|
||||||
|
accuracy: location?.coords?.accuracy,
|
||||||
|
latitude: location?.coords?.latitude,
|
||||||
|
longitude: location?.coords?.longitude,
|
||||||
|
timestamp: location?.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If uploads are currently disabled (pre-login), we'll flush this record once auth
|
||||||
|
// becomes available.
|
||||||
|
|
||||||
|
// If uploads are enabled, proactively flush now to guarantee server receives the
|
||||||
|
// first point quickly even if the SDK doesn't auto-sync immediately.
|
||||||
|
if (authReady && !didSyncAfterStartupFix) {
|
||||||
|
const ok = await safeSync("startup-fix");
|
||||||
|
if (ok) didSyncAfterStartupFix = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
locationLogger.warn("Startup persisted fix failed", {
|
||||||
|
error: error?.message,
|
||||||
|
code: error?.code,
|
||||||
|
stack: error?.stack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// When auth changes, we want a fresh persisted point for the newly effective identity.
|
// When auth changes, we want a fresh persisted point for the newly effective identity.
|
||||||
// Debounced to avoid spamming `getCurrentPosition` if auth updates quickly (refresh/renew).
|
// Debounced to avoid spamming `getCurrentPosition` if auth updates quickly (refresh/renew).
|
||||||
let authFixDebounceTimerId = null;
|
let authFixDebounceTimerId = null;
|
||||||
let authFixInFlight = null;
|
let authFixInFlight = null;
|
||||||
const AUTH_FIX_DEBOUNCE_MS = 1500;
|
const AUTH_FIX_DEBOUNCE_MS = 1500;
|
||||||
|
const AUTH_FIX_COOLDOWN_MS = 15 * 60 * 1000;
|
||||||
|
let lastAuthFixAt = 0;
|
||||||
|
|
||||||
const scheduleAuthFreshFix = () => {
|
const scheduleAuthFreshFix = () => {
|
||||||
|
// Avoid generating persisted + auto-synced locations as a side-effect of frequent
|
||||||
|
// auth refreshes (eg app resume / screen unlock).
|
||||||
|
if (Date.now() - lastAuthFixAt < AUTH_FIX_COOLDOWN_MS) {
|
||||||
|
return authFixInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
if (authFixDebounceTimerId) {
|
if (authFixDebounceTimerId) {
|
||||||
clearTimeout(authFixDebounceTimerId);
|
clearTimeout(authFixDebounceTimerId);
|
||||||
authFixDebounceTimerId = null;
|
authFixDebounceTimerId = null;
|
||||||
|
|
@ -74,23 +224,47 @@ export default function trackLocation() {
|
||||||
isMoving: before.isMoving,
|
isMoving: before.isMoving,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we're already in ACTIVE, the profile transition will request an immediate
|
||||||
|
// high-accuracy persisted fix. Avoid duplicating work here.
|
||||||
|
if (currentProfile === "active") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const location = await BackgroundGeolocation.getCurrentPosition({
|
const location = await BackgroundGeolocation.getCurrentPosition({
|
||||||
samples: 3,
|
samples: 1,
|
||||||
persist: true,
|
// IMPORTANT: do not persist by default.
|
||||||
timeout: 30,
|
// Persisting will create a DB record and the SDK may upload it on resume,
|
||||||
maximumAge: 5000,
|
// which is the source of "updates while not moved" on some devices.
|
||||||
desiredAccuracy: 50,
|
persist: false,
|
||||||
|
timeout: 20,
|
||||||
|
maximumAge: 10000,
|
||||||
|
desiredAccuracy: 100,
|
||||||
extras: {
|
extras: {
|
||||||
auth_token_update: true,
|
auth_token_update: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If the fix is very poor accuracy, treat it as noise and do nothing.
|
||||||
|
// (We intentionally do not persist in this path.)
|
||||||
|
const acc = location?.coords?.accuracy;
|
||||||
|
if (typeof acc === "number" && acc > 100) {
|
||||||
|
locationLogger.info(
|
||||||
|
"Auth-change fix ignored due to poor accuracy",
|
||||||
|
{
|
||||||
|
accuracy: acc,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
locationLogger.info("Auth-change location fix acquired", {
|
locationLogger.info("Auth-change location fix acquired", {
|
||||||
accuracy: location?.coords?.accuracy,
|
accuracy: location?.coords?.accuracy,
|
||||||
latitude: location?.coords?.latitude,
|
latitude: location?.coords?.latitude,
|
||||||
longitude: location?.coords?.longitude,
|
longitude: location?.coords?.longitude,
|
||||||
timestamp: location?.timestamp,
|
timestamp: location?.timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
lastAuthFixAt = Date.now();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
locationLogger.warn("Auth-change location fix failed", {
|
locationLogger.warn("Auth-change location fix failed", {
|
||||||
error: error?.message,
|
error: error?.message,
|
||||||
|
|
@ -238,25 +412,52 @@ export default function trackLocation() {
|
||||||
locationLogger.info("Handling auth token update", {
|
locationLogger.info("Handling auth token update", {
|
||||||
hasToken: !!userToken,
|
hasToken: !!userToken,
|
||||||
});
|
});
|
||||||
if (!userToken) {
|
|
||||||
locationLogger.info("No auth token, stopping location tracking");
|
|
||||||
|
|
||||||
// Prevent any further uploads before stopping.
|
// Compute identity from session store; this is our source of truth.
|
||||||
// This guards against persisted HTTP config continuing to flush queued records.
|
// (A token refresh for the same user should not force a new persisted fix.)
|
||||||
|
let currentSessionUserId = null;
|
||||||
|
try {
|
||||||
|
currentSessionUserId = getSessionState()?.userId ?? null;
|
||||||
|
} catch (e) {
|
||||||
|
currentSessionUserId = null;
|
||||||
|
}
|
||||||
|
if (!userToken) {
|
||||||
|
// Pre-login mode: keep tracking enabled but disable uploads.
|
||||||
|
// Also applies to logout: keep tracking on (per product requirement: track all the time),
|
||||||
|
// but stop sending anything to server without auth.
|
||||||
|
locationLogger.info(
|
||||||
|
"No auth token: disabling BGGeo uploads (keeping tracking on)",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await BackgroundGeolocation.setConfig({
|
await BackgroundGeolocation.setConfig({
|
||||||
url: "",
|
url: "",
|
||||||
autoSync: false,
|
autoSync: false,
|
||||||
headers: {},
|
headers: {},
|
||||||
});
|
});
|
||||||
|
didDisableUploadsForAnonymous = true;
|
||||||
|
didSyncAfterAuth = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
locationLogger.warn("Failed to clear BGGeo HTTP config on logout", {
|
locationLogger.warn("Failed to disable BGGeo uploads (anonymous)", {
|
||||||
error: e?.message,
|
error: e?.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await BackgroundGeolocation.stop();
|
const state = await BackgroundGeolocation.getState();
|
||||||
locationLogger.debug("Location tracking stopped");
|
if (!state.enabled) {
|
||||||
|
try {
|
||||||
|
await BackgroundGeolocation.start();
|
||||||
|
locationLogger.debug("Location tracking started in anonymous mode");
|
||||||
|
} catch (error) {
|
||||||
|
locationLogger.error(
|
||||||
|
"Failed to start location tracking in anonymous mode",
|
||||||
|
{
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup subscriptions when logged out.
|
// Cleanup subscriptions when logged out.
|
||||||
try {
|
try {
|
||||||
|
|
@ -275,12 +476,22 @@ export default function trackLocation() {
|
||||||
authFixDebounceTimerId = null;
|
authFixDebounceTimerId = null;
|
||||||
}
|
}
|
||||||
authFixInFlight = null;
|
authFixInFlight = null;
|
||||||
|
|
||||||
|
// Still request a one-time persisted fix at startup in anonymous mode so we have
|
||||||
|
// something to flush immediately after auth.
|
||||||
|
if (!didRequestStartupFix) {
|
||||||
|
didRequestStartupFix = true;
|
||||||
|
startupFixInFlight = requestStartupPersistedFix();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSessionUserId = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// unsub();
|
// unsub();
|
||||||
locationLogger.debug("Updating background geolocation config");
|
locationLogger.debug("Updating background geolocation config");
|
||||||
await BackgroundGeolocation.setConfig({
|
await BackgroundGeolocation.setConfig({
|
||||||
url: env.GEOLOC_SYNC_URL, // Update the sync URL for when it's changed for staging
|
url: env.GEOLOC_SYNC_URL, // Update the sync URL for when it's changed for staging
|
||||||
|
autoSync: true,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${userToken}`,
|
Authorization: `Bearer ${userToken}`,
|
||||||
},
|
},
|
||||||
|
|
@ -320,6 +531,38 @@ export default function trackLocation() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If identity has changed (including first login), force a persisted fix for this identity
|
||||||
|
// and sync immediately so the new identity has an immediate first geopoint.
|
||||||
|
if (currentSessionUserId && currentSessionUserId !== lastSessionUserId) {
|
||||||
|
const reason = lastSessionUserId ? "user-switch" : "first-login";
|
||||||
|
locationLogger.info("Identity change detected", {
|
||||||
|
reason,
|
||||||
|
from: lastSessionUserId,
|
||||||
|
to: currentSessionUserId,
|
||||||
|
});
|
||||||
|
lastSessionUserId = currentSessionUserId;
|
||||||
|
await requestIdentityPersistedFixAndSync({
|
||||||
|
reason,
|
||||||
|
userId: currentSessionUserId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we were previously in anonymous mode, flush any queued persisted locations now.
|
||||||
|
if (didDisableUploadsForAnonymous && !didSyncAfterAuth) {
|
||||||
|
try {
|
||||||
|
if (startupFixInFlight) {
|
||||||
|
await startupFixInFlight;
|
||||||
|
}
|
||||||
|
const ok = await safeSync("pre-auth-flush");
|
||||||
|
didSyncAfterAuth = ok;
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.warn("Pre-auth flush failed", {
|
||||||
|
error: e?.message,
|
||||||
|
stack: e?.stack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Always request a fresh persisted point on any token update.
|
// Always request a fresh persisted point on any token update.
|
||||||
// This ensures a newly connected user gets an immediate point even if they don't move.
|
// This ensures a newly connected user gets an immediate point even if they don't move.
|
||||||
scheduleAuthFreshFix();
|
scheduleAuthFreshFix();
|
||||||
|
|
@ -329,7 +572,7 @@ export default function trackLocation() {
|
||||||
// - We do NOT force moving mode.
|
// - We do NOT force moving mode.
|
||||||
if (!didRequestStartupFix) {
|
if (!didRequestStartupFix) {
|
||||||
didRequestStartupFix = true;
|
didRequestStartupFix = true;
|
||||||
startupFixInFlight = scheduleAuthFreshFix();
|
startupFixInFlight = requestStartupPersistedFix();
|
||||||
} else if (authFixInFlight) {
|
} else if (authFixInFlight) {
|
||||||
// Avoid concurrent fix calls if auth updates race.
|
// Avoid concurrent fix calls if auth updates race.
|
||||||
await authFixInFlight;
|
await authFixInFlight;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue