chore: try to use sync to refresh

This commit is contained in:
devthejo 2025-07-01 13:39:38 +02:00
parent 6af58755c1
commit e47a33bcd8
6 changed files with 377 additions and 268 deletions

View file

@ -20,6 +20,7 @@ import onMessageReceived from "~/notifications/onMessageReceived";
import { createLogger } from "~/lib/logger";
import * as Sentry from "@sentry/react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { handleHttpResponse } from "~/lib/geolocation/httpResponseHandler";
// setup notification, this have to stay in index.js
notifee.onBackgroundEvent(notificationBackgroundEvent);
@ -33,7 +34,8 @@ registerRootComponent(App);
// Constants for persistence
const LAST_SYNC_TIME_KEY = "@geolocation_last_sync_time";
// const FORCE_SYNC_INTERVAL = 24 * 60 * 60 * 1000;
const FORCE_SYNC_INTERVAL = 60 * 60 * 1000; // DEBUGGING
// const FORCE_SYNC_INTERVAL = 60 * 60 * 1000; // DEBUGGING
const FORCE_SYNC_INTERVAL = 5 * 60 * 1000; // DEBUGGING
// Helper functions for persisting sync time
const getLastSyncTime = async () => {
@ -329,54 +331,47 @@ const HeadlessTask = async (event) => {
break;
case "http":
// Validate HTTP parameters
if (!params || typeof params !== "object" || !params.response) {
geolocBgLogger.warn("Invalid HTTP params", { params });
break;
}
try {
// Use shared HTTP response handler for headless context
await handleHttpResponse(params, "headless");
const httpStatus = params.response?.status;
const isHttpSuccess = httpStatus === 200;
// Update last sync time on successful HTTP response (keep existing logic)
const httpStatus = params.status;
if (httpStatus === 200) {
try {
const now = Date.now();
await setLastSyncTime(now);
Sentry.addBreadcrumb({
message: "HTTP response received",
category: "headless-task",
level: isHttpSuccess ? "info" : "warning",
data: {
status: httpStatus,
success: params.response?.success,
hasResponse: !!params.response,
},
});
Sentry.addBreadcrumb({
message: "Last sync time updated (HTTP success)",
category: "headless-task",
level: "info",
data: { newSyncTime: new Date(now).toISOString() },
});
} catch (syncTimeError) {
geolocBgLogger.error("Failed to update sync time", {
error: syncTimeError,
});
geolocBgLogger.debug("HTTP response received", {
response: params.response,
});
// Update last sync time on successful HTTP response
if (isHttpSuccess) {
try {
const now = Date.now();
await setLastSyncTime(now);
Sentry.addBreadcrumb({
message: "Last sync time updated (HTTP success)",
category: "headless-task",
level: "info",
data: { newSyncTime: new Date(now).toISOString() },
});
} catch (syncTimeError) {
geolocBgLogger.error("Failed to update sync time", {
error: syncTimeError,
});
Sentry.captureException(syncTimeError, {
tags: {
module: "headless-task",
operation: "update-sync-time-http",
},
});
Sentry.captureException(syncTimeError, {
tags: {
module: "headless-task",
operation: "update-sync-time-http",
},
});
}
}
} catch (httpHandlerError) {
geolocBgLogger.error("HTTP response handler failed", {
error: httpHandlerError,
});
Sentry.captureException(httpHandlerError, {
tags: {
module: "headless-task",
operation: "http-response-handler",
},
});
}
break;

View file

@ -0,0 +1,225 @@
import { createLogger } from "~/lib/logger";
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
import * as Sentry from "@sentry/react-native";
import BackgroundGeolocation from "react-native-background-geolocation";
import throttle from "lodash.throttle";
import { authActions, getAuthState } from "~/stores";
import { setLastSyncTime } from "~/lib/geolocation/syncTimeManager";
// Create logger for HTTP response handling
const httpLogger = createLogger({
module: BACKGROUND_SCOPES.GEOLOCATION,
feature: "http-handler",
});
// Throttling configuration for auth reload
const AUTH_RELOAD_THROTTLE = 5000; // 5 seconds throttle
// The core auth reload function that will be throttled
async function _reloadAuth() {
httpLogger.info("Refreshing authentication token via sync endpoint");
try {
// Get current auth state to check if we have an auth token
const { authToken, userToken } = getAuthState();
if (!authToken) {
httpLogger.warn("No auth token available for refresh");
return;
}
httpLogger.debug(
"Auth token available, updating BackgroundGeolocation config",
);
// Update BackgroundGeolocation config to include X-Auth-Token header
await BackgroundGeolocation.setConfig({
headers: {
Authorization: `Bearer ${userToken}`, // Keep existing user token (may be expired)
"X-Auth-Token": authToken, // Add auth token for refresh
},
});
// Trigger sync to refresh token
await BackgroundGeolocation.changePace(true);
await BackgroundGeolocation.sync();
httpLogger.info("Token refresh sync triggered successfully");
} catch (error) {
httpLogger.error("Failed to refresh authentication token", {
error: error.message,
stack: error.stack,
});
}
}
// Create throttled version of auth reload with lodash
const reloadAuth = throttle(_reloadAuth, AUTH_RELOAD_THROTTLE, {
leading: true,
trailing: false, // Prevent trailing calls to avoid duplicate refreshes
});
/**
* Shared HTTP response handler for both foreground and headless contexts
* @param {Object} response - The HTTP response object from BackgroundGeolocation
* @param {string} context - Either 'foreground' or 'headless'
*/
export const handleHttpResponse = async (response, context = "foreground") => {
// Log the full response including headers if available
httpLogger.debug("HTTP response received", {
status: response?.status,
success: response?.success,
responseText: response?.responseText,
url: response?.url,
method: response?.method,
isSync: response?.isSync,
context,
requestHeaders:
response?.request?.headers || "Headers not available in response",
});
// Add Sentry breadcrumb for HTTP responses
Sentry.addBreadcrumb({
message: `Background geolocation HTTP response (${context})`,
category: "geolocation-http",
level: response?.status === 200 ? "info" : "warning",
data: {
status: response?.status,
success: response?.success,
url: response?.url,
isSync: response?.isSync,
recordCount: response?.count,
context,
},
});
const statusCode = response?.status;
// Log status code and response
httpLogger.debug("Processing HTTP response", {
status: statusCode,
responseText: response?.responseText,
context,
});
switch (statusCode) {
case 200:
await handleSuccessResponse(response, context);
break;
case 410:
await handleAuthTokenNotFound(response, context);
break;
case 401:
await handleUnauthorized(response, context);
break;
default:
httpLogger.debug("Unhandled HTTP status code", {
status: statusCode,
context,
});
}
};
/**
* Handle successful HTTP response (status 200)
*/
const handleSuccessResponse = async (response, context) => {
try {
const responseBody = response?.responseText
? JSON.parse(response.responseText)
: null;
if (responseBody?.userBearerJwt) {
httpLogger.info("Token refresh successful, updating stored token", {
context,
});
// Use auth action to update both in-memory and persistent storage
await authActions.setUserToken(responseBody.userBearerJwt);
// Update BackgroundGeolocation config with new token and remove X-Auth-Token header
await BackgroundGeolocation.setConfig({
headers: {
Authorization: `Bearer ${responseBody.userBearerJwt}`,
},
});
httpLogger.debug(
"Updated BackgroundGeolocation with refreshed token and removed X-Auth-Token header",
{ context },
);
Sentry.addBreadcrumb({
message: "Token refreshed successfully",
category: "geolocation-auth",
level: "info",
data: { context },
});
}
} catch (e) {
httpLogger.debug("Failed to parse successful response", {
error: e.message,
responseText: response?.responseText,
context,
});
}
};
/**
* Handle auth token expired (status 410)
*/
const handleAuthTokenNotFound = async (response, context) => {
httpLogger.info("Auth token not found (410), logging out", { context });
Sentry.addBreadcrumb({
message: "Auth token expired - logging out",
category: "geolocation-auth",
level: "warning",
data: { context },
});
authActions.logout();
};
/**
* Handle unauthorized request (status 401)
*/
const handleUnauthorized = async (response, context) => {
httpLogger.info("Unauthorized (401), attempting to refresh token", {
context,
});
// Add more detailed logging of the error response
try {
const errorBody = response?.responseText
? JSON.parse(response.responseText)
: null;
httpLogger.debug("Unauthorized error details", {
errorBody,
errorType: errorBody?.error?.type,
errorMessage: errorBody?.error?.message,
errorPath: errorBody?.error?.errors?.[0]?.path,
context,
});
Sentry.addBreadcrumb({
message: "Unauthorized - refreshing token",
category: "geolocation-auth",
level: "warning",
data: {
errorType: errorBody?.error?.type,
errorMessage: errorBody?.error?.message,
context,
},
});
} catch (e) {
httpLogger.debug("Failed to parse error response", {
error: e.message,
responseText: response?.responseText,
context,
});
}
await reloadAuth();
};

View file

@ -0,0 +1,67 @@
import { memoryAsyncStorage } from "~/lib/memoryAsyncStorage";
import { createLogger } from "~/lib/logger";
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
import * as Sentry from "@sentry/react-native";
// Constants for persistence
const LAST_SYNC_TIME_KEY = "@geolocation_last_sync_time";
// Create logger for sync time management
const syncTimeLogger = createLogger({
module: BACKGROUND_SCOPES.GEOLOCATION,
feature: "sync-time",
});
/**
* Get the last sync time from storage
* @returns {Promise<number>} The last sync time in milliseconds, or current time if not found
*/
export const getLastSyncTime = async () => {
try {
const value = await memoryAsyncStorage.getItem(LAST_SYNC_TIME_KEY);
const lastSyncTime = value ? parseInt(value, 10) : Date.now();
syncTimeLogger.debug("Retrieved last sync time", {
value,
lastSyncTime,
isDefault: !value,
});
return lastSyncTime;
} catch (error) {
syncTimeLogger.error("Failed to get last sync time", {
error: error.message,
});
Sentry.captureException(error, {
tags: { module: "sync-time-manager", operation: "get-last-sync-time" },
});
// Return current time as fallback
return Date.now();
}
};
/**
* Set the last sync time in storage
* @param {number} time - The sync time in milliseconds
*/
export const setLastSyncTime = async (time) => {
try {
await memoryAsyncStorage.setItem(LAST_SYNC_TIME_KEY, time.toString());
syncTimeLogger.debug("Set last sync time", {
time,
timeISO: new Date(time).toISOString(),
});
} catch (error) {
syncTimeLogger.error("Failed to set last sync time", {
error: error.message,
time,
});
Sentry.captureException(error, {
tags: { module: "sync-time-manager", operation: "set-last-sync-time" },
});
}
};

View file

@ -152,19 +152,20 @@ export const memoryAsyncStorage = {
storageLogger.debug("Set in memory cache", { key });
// Try to persist to AsyncStorage
try {
await AsyncStorage.setItem(key, value);
storageLogger.debug("Persisted to AsyncStorage", { key });
} catch (error) {
storageLogger.warn(
"Failed to persist to AsyncStorage, kept in memory only",
{
key,
error: error.message,
},
);
// Continue - value is at least in memory
}
(async () => {
try {
await AsyncStorage.setItem(key, value);
storageLogger.debug("Persisted to AsyncStorage", { key });
} catch (error) {
storageLogger.warn(
"Failed to persist to AsyncStorage, kept in memory only",
{
key,
error: error.message,
},
);
}
})();
},
/**
@ -178,16 +179,18 @@ export const memoryAsyncStorage = {
storageLogger.debug("Deleted from memory cache", { key });
// Try to delete from AsyncStorage
try {
await AsyncStorage.removeItem(key);
storageLogger.debug("Deleted from AsyncStorage", { key });
} catch (error) {
storageLogger.warn("Failed to delete from AsyncStorage", {
key,
error: error.message,
});
// Continue - at least removed from memory
}
(async () => {
try {
await AsyncStorage.removeItem(key);
storageLogger.debug("Deleted from AsyncStorage", { key });
} catch (error) {
storageLogger.warn("Failed to delete from AsyncStorage", {
key,
error: error.message,
});
// Continue - at least removed from memory
}
})();
},
/**
@ -242,14 +245,16 @@ export const memoryAsyncStorage = {
storageLogger.info("Cleared memory cache");
// Try to clear AsyncStorage
try {
await AsyncStorage.clear();
storageLogger.info("Cleared AsyncStorage");
} catch (error) {
storageLogger.warn("Failed to clear AsyncStorage", {
error: error.message,
});
}
(async () => {
try {
await AsyncStorage.clear();
storageLogger.info("Cleared AsyncStorage");
} catch (error) {
storageLogger.warn("Failed to clear AsyncStorage", {
error: error.message,
});
}
})();
},
/**

View file

@ -18,6 +18,7 @@ import {
import setLocationState from "~/location/setLocationState";
import { storeLocation } from "~/utils/location/storage";
import { handleHttpResponse } from "~/lib/geolocation/httpResponseHandler";
import env from "~/env";
@ -76,53 +77,6 @@ export default async function trackLocation() {
isStaging: env.IS_STAGING,
});
// Throttling configuration for auth reload only
const AUTH_RELOAD_THROTTLE = 5000; // 5 seconds throttle
// The core auth reload function that will be throttled
async function _reloadAuth() {
locationLogger.info("Refreshing authentication token via sync endpoint");
try {
// Get current auth state to check if we have an auth token
const { authToken, userToken } = getAuthState();
if (!authToken) {
locationLogger.warn("No auth token available for refresh");
return;
}
locationLogger.debug(
"Auth token available, updating BackgroundGeolocation config",
);
// Update BackgroundGeolocation config to include X-Auth-Token header
await BackgroundGeolocation.setConfig({
headers: {
Authorization: `Bearer ${userToken}`, // Keep existing user token (may be expired)
"X-Auth-Token": authToken, // Add auth token for refresh
},
});
// Trigger sync to refresh token
await BackgroundGeolocation.changePace(true);
await BackgroundGeolocation.sync();
locationLogger.info("Token refresh sync triggered successfully");
} catch (error) {
locationLogger.error("Failed to refresh authentication token", {
error: error.message,
stack: error.stack,
});
}
}
// Create throttled version of auth reload with lodash
const reloadAuth = throttle(_reloadAuth, AUTH_RELOAD_THROTTLE, {
leading: true,
trailing: false, // Prevent trailing calls to avoid duplicate refreshes
});
// Handle auth function - no throttling or cooldown
async function handleAuth(userToken) {
locationLogger.info("Handling auth token update", {
@ -152,25 +106,6 @@ export default async function trackLocation() {
},
);
// Verify the current configuration
try {
const currentConfig = await BackgroundGeolocation.getConfig();
locationLogger.debug("Current background geolocation config", {
hasHeaders: !!currentConfig.headers,
headerKeys: currentConfig.headers
? Object.keys(currentConfig.headers)
: [],
authHeader: currentConfig.headers?.Authorization
? currentConfig.headers.Authorization.substring(0, 15) + "..."
: "Not set",
url: currentConfig.url,
});
} catch (error) {
locationLogger.error("Failed to get background geolocation config", {
error: error.message,
});
}
const state = await BackgroundGeolocation.getState();
try {
const decodedToken = jwtDecode(userToken);
@ -240,132 +175,8 @@ export default async function trackLocation() {
});
BackgroundGeolocation.onHttp(async (response) => {
// Log the full response including headers if available
locationLogger.debug("HTTP response received", {
status: response?.status,
success: response?.success,
responseText: response?.responseText,
url: response?.url,
method: response?.method,
isSync: response?.isSync,
requestHeaders:
response?.request?.headers || "Headers not available in response",
});
// Add Sentry breadcrumb for HTTP responses
Sentry.addBreadcrumb({
message: "Background geolocation HTTP response",
category: "geolocation-http",
level: response?.status === 200 ? "info" : "warning",
data: {
status: response?.status,
success: response?.success,
url: response?.url,
isSync: response?.isSync,
recordCount: response?.count,
},
});
// Log the current auth token for comparison
const { userToken } = getAuthState();
locationLogger.debug("Current auth state token", {
tokenAvailable: !!userToken,
tokenPrefix: userToken ? userToken.substring(0, 10) + "..." : null,
});
const statusCode = response?.status;
// log status code and response
locationLogger.debug("HTTP response received", {
status: statusCode,
responseText: response?.responseText,
});
switch (statusCode) {
case 200:
// Successful response, check for token refresh
try {
const responseBody = response?.responseText
? JSON.parse(response.responseText)
: null;
if (responseBody?.userBearerJwt) {
locationLogger.info(
"Token refresh successful, updating stored token",
);
// Use auth action to update both in-memory and persistent storage
await authActions.setUserToken(responseBody.userBearerJwt);
// Update BackgroundGeolocation config with new token
await BackgroundGeolocation.setConfig({
headers: {
Authorization: `Bearer ${responseBody.userBearerJwt}`,
},
});
locationLogger.debug(
"Updated BackgroundGeolocation with refreshed token and removed X-Auth-Token header",
);
Sentry.addBreadcrumb({
message: "Token refreshed successfully",
category: "geolocation-auth",
level: "info",
});
}
} catch (e) {
locationLogger.debug("Failed to parse successful response", {
error: e.message,
responseText: response?.responseText,
});
}
break;
case 410:
// Auth token expired, logout
locationLogger.info("Auth token expired (410), logging out");
Sentry.addBreadcrumb({
message: "Auth token expired - logging out",
category: "geolocation-auth",
level: "warning",
});
authActions.logout();
break;
case 401:
// Unauthorized: User token expired, refresh using throttled reload
locationLogger.info("Unauthorized (401), attempting to refresh token");
// Add more detailed logging of the error response
try {
const errorBody = response?.responseText
? JSON.parse(response.responseText)
: null;
locationLogger.debug("Unauthorized error details", {
errorBody,
errorType: errorBody?.error?.type,
errorMessage: errorBody?.error?.message,
errorPath: errorBody?.error?.errors?.[0]?.path,
});
Sentry.addBreadcrumb({
message: "Unauthorized - refreshing token",
category: "geolocation-auth",
level: "warning",
data: {
errorType: errorBody?.error?.type,
errorMessage: errorBody?.error?.message,
},
});
} catch (e) {
locationLogger.debug("Failed to parse error response", {
error: e.message,
responseText: response?.responseText,
});
}
reloadAuth();
break;
}
// Use shared HTTP response handler for foreground context
await handleHttpResponse(response, "foreground");
});
try {

View file

@ -71,6 +71,7 @@ export default createAtom(({ get, merge, getActions }) => {
await secureStore.setItemAsync("userToken", userToken);
endLoading({
userToken,
authToken,
});
sessionActions.loadSessionFromJWT(userToken);
return { userToken };
@ -110,6 +111,7 @@ export default createAtom(({ get, merge, getActions }) => {
} else {
endLoading({
userToken,
authToken,
});
sessionActions.loadSessionFromJWT(jwtData);
return;
@ -122,6 +124,7 @@ export default createAtom(({ get, merge, getActions }) => {
authLogger.info("Successfully registered new user");
authToken = res.authToken;
await secureStore.setItemAsync("authToken", authToken);
merge({ authToken });
}
if (!userToken && authToken) {
@ -260,6 +263,8 @@ export default createAtom(({ get, merge, getActions }) => {
]);
merge({
userOffMode: true,
authToken: null,
userToken: null,
});
return;
}
@ -303,6 +308,7 @@ export default createAtom(({ get, merge, getActions }) => {
return {
default: {
userToken: null,
authToken: null,
loading: true,
initialized: false,
onReload: false,