435 lines
13 KiB
JavaScript
435 lines
13 KiB
JavaScript
// debug
|
|
import "./wdyr"; // <--- first import
|
|
import "./warnFilter";
|
|
|
|
import "expo-splash-screen";
|
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
|
|
|
import notifee from "@notifee/react-native";
|
|
import messaging from "@react-native-firebase/messaging";
|
|
|
|
import "~/sentry";
|
|
|
|
import { registerRootComponent } from "expo";
|
|
|
|
import App from "~/app";
|
|
|
|
import { onBackgroundEvent as notificationBackgroundEvent } from "~/notifications/onEvent";
|
|
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";
|
|
|
|
// setup notification, this have to stay in index.js
|
|
notifee.onBackgroundEvent(notificationBackgroundEvent);
|
|
messaging().setBackgroundMessageHandler(onMessageReceived);
|
|
|
|
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
|
// It also ensures that whether you load the app in Expo Go or in a native build,
|
|
// the environment is set up appropriately
|
|
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
|
|
|
|
// Helper functions for persisting sync time
|
|
const getLastSyncTime = async () => {
|
|
try {
|
|
const value = await AsyncStorage.getItem(LAST_SYNC_TIME_KEY);
|
|
return value ? parseInt(value, 10) : Date.now();
|
|
} catch (error) {
|
|
Sentry.captureException(error, {
|
|
tags: { module: "headless-task", operation: "get-last-sync-time" },
|
|
});
|
|
return Date.now();
|
|
}
|
|
};
|
|
|
|
const setLastSyncTime = async (time) => {
|
|
try {
|
|
await AsyncStorage.setItem(LAST_SYNC_TIME_KEY, time.toString());
|
|
} catch (error) {
|
|
Sentry.captureException(error, {
|
|
tags: { module: "headless-task", operation: "set-last-sync-time" },
|
|
});
|
|
}
|
|
};
|
|
|
|
// this have to stay in index.js, see also https://github.com/transistorsoft/react-native-background-geolocation/wiki/Android-Headless-Mode
|
|
const getCurrentPosition = () => {
|
|
return new Promise((resolve) => {
|
|
// Add timeout protection
|
|
const timeout = setTimeout(() => {
|
|
resolve({ code: -1, message: "getCurrentPosition timeout" });
|
|
}, 15000); // 15 second timeout
|
|
|
|
BackgroundGeolocation.getCurrentPosition(
|
|
{
|
|
samples: 1,
|
|
persist: true,
|
|
extras: { background: true },
|
|
timeout: 10, // 10 second timeout in the plugin itself
|
|
},
|
|
(location) => {
|
|
clearTimeout(timeout);
|
|
resolve(location);
|
|
},
|
|
(error) => {
|
|
clearTimeout(timeout);
|
|
resolve(error);
|
|
},
|
|
);
|
|
});
|
|
};
|
|
|
|
const geolocBgLogger = createLogger({
|
|
service: "background-geolocation",
|
|
task: "headless",
|
|
});
|
|
|
|
const HeadlessTask = async (event) => {
|
|
// Add timeout protection for the entire headless task
|
|
const taskTimeout = setTimeout(() => {
|
|
geolocBgLogger.error("HeadlessTask timeout", { event });
|
|
|
|
Sentry.captureException(new Error("HeadlessTask timeout"), {
|
|
tags: {
|
|
module: "background-geolocation",
|
|
operation: "headless-task-timeout",
|
|
eventName: event?.name,
|
|
},
|
|
});
|
|
}, 60000); // 60 second timeout
|
|
|
|
// Simple performance tracking without deprecated APIs
|
|
const taskStartTime = Date.now();
|
|
|
|
try {
|
|
// Validate event structure
|
|
if (!event || typeof event !== "object") {
|
|
throw new Error("Invalid event object received");
|
|
}
|
|
|
|
const { name, params } = event;
|
|
|
|
if (!name || typeof name !== "string") {
|
|
throw new Error("Invalid event name received");
|
|
}
|
|
|
|
// Add initial breadcrumb
|
|
Sentry.addBreadcrumb({
|
|
message: "HeadlessTask started",
|
|
category: "headless-task",
|
|
level: "info",
|
|
data: {
|
|
eventName: name,
|
|
params: params ? JSON.stringify(params) : null,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
|
|
geolocBgLogger.info("HeadlessTask event received", { name, params });
|
|
|
|
switch (name) {
|
|
case "heartbeat":
|
|
// Add breadcrumb for heartbeat event
|
|
Sentry.addBreadcrumb({
|
|
message: "Heartbeat event received",
|
|
category: "headless-task",
|
|
level: "info",
|
|
timestamp: Date.now() / 1000,
|
|
});
|
|
|
|
// Get persisted last sync time
|
|
const lastSyncTime = await getLastSyncTime();
|
|
const now = Date.now();
|
|
const timeSinceLastSync = now - lastSyncTime;
|
|
|
|
// Add context about sync timing
|
|
Sentry.setContext("sync-timing", {
|
|
lastSyncTime: new Date(lastSyncTime).toISOString(),
|
|
currentTime: new Date(now).toISOString(),
|
|
timeSinceLastSync: timeSinceLastSync,
|
|
timeSinceLastSyncHours: (
|
|
timeSinceLastSync /
|
|
(1000 * 60 * 60)
|
|
).toFixed(2),
|
|
needsForceSync: timeSinceLastSync >= FORCE_SYNC_INTERVAL,
|
|
});
|
|
|
|
Sentry.addBreadcrumb({
|
|
message: "Sync timing calculated",
|
|
category: "headless-task",
|
|
level: "info",
|
|
data: {
|
|
timeSinceLastSyncHours: (
|
|
timeSinceLastSync /
|
|
(1000 * 60 * 60)
|
|
).toFixed(2),
|
|
needsForceSync: timeSinceLastSync >= FORCE_SYNC_INTERVAL,
|
|
},
|
|
});
|
|
|
|
// Get current position with performance tracking
|
|
const locationStartTime = Date.now();
|
|
const location = await getCurrentPosition();
|
|
const locationDuration = Date.now() - locationStartTime;
|
|
|
|
const isLocationError = location && location.code !== undefined;
|
|
|
|
Sentry.addBreadcrumb({
|
|
message: "getCurrentPosition completed",
|
|
category: "headless-task",
|
|
level: isLocationError ? "warning" : "info",
|
|
data: {
|
|
success: !isLocationError,
|
|
error: isLocationError ? location : undefined,
|
|
coords: !isLocationError ? location?.coords : undefined,
|
|
},
|
|
});
|
|
|
|
geolocBgLogger.debug("getCurrentPosition result", { location });
|
|
|
|
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
|
|
geolocBgLogger.info("Forcing location sync after 24h");
|
|
|
|
Sentry.addBreadcrumb({
|
|
message: "Force sync triggered",
|
|
category: "headless-task",
|
|
level: "info",
|
|
data: {
|
|
timeSinceLastSyncHours: (
|
|
timeSinceLastSync /
|
|
(1000 * 60 * 60)
|
|
).toFixed(2),
|
|
},
|
|
});
|
|
|
|
try {
|
|
// Get pending records count before sync with timeout
|
|
const pendingCount = await Promise.race([
|
|
BackgroundGeolocation.getCount(),
|
|
new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error("getCount timeout")), 10000),
|
|
),
|
|
]);
|
|
|
|
Sentry.addBreadcrumb({
|
|
message: "Pending records count",
|
|
category: "headless-task",
|
|
level: "info",
|
|
data: { pendingCount },
|
|
});
|
|
|
|
// Change pace to ensure location updates with timeout
|
|
await Promise.race([
|
|
BackgroundGeolocation.changePace(true),
|
|
new Promise((_, reject) =>
|
|
setTimeout(
|
|
() => reject(new Error("changePace timeout")),
|
|
10000,
|
|
),
|
|
),
|
|
]);
|
|
|
|
Sentry.addBreadcrumb({
|
|
message: "changePace completed",
|
|
category: "headless-task",
|
|
level: "info",
|
|
});
|
|
|
|
// Perform sync with timeout
|
|
const syncResult = await Promise.race([
|
|
BackgroundGeolocation.sync(),
|
|
new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error("sync timeout")), 20000),
|
|
),
|
|
]);
|
|
|
|
Sentry.addBreadcrumb({
|
|
message: "Sync completed successfully",
|
|
category: "headless-task",
|
|
level: "info",
|
|
data: {
|
|
syncResult: Array.isArray(syncResult)
|
|
? `${syncResult.length} records`
|
|
: "completed",
|
|
},
|
|
});
|
|
|
|
// Update last sync time after successful sync
|
|
await setLastSyncTime(now);
|
|
|
|
Sentry.addBreadcrumb({
|
|
message: "Last sync time updated",
|
|
category: "headless-task",
|
|
level: "info",
|
|
data: { newSyncTime: new Date(now).toISOString() },
|
|
});
|
|
} catch (syncError) {
|
|
Sentry.captureException(syncError, {
|
|
tags: {
|
|
module: "headless-task",
|
|
operation: "force-sync",
|
|
eventName: name,
|
|
},
|
|
contexts: {
|
|
syncAttempt: {
|
|
timeSinceLastSync: timeSinceLastSync,
|
|
lastSyncTime: new Date(lastSyncTime).toISOString(),
|
|
},
|
|
},
|
|
});
|
|
|
|
geolocBgLogger.error("Force sync failed", { error: syncError });
|
|
}
|
|
} else {
|
|
Sentry.addBreadcrumb({
|
|
message: "Force sync not needed",
|
|
category: "headless-task",
|
|
level: "info",
|
|
data: {
|
|
timeSinceLastSyncHours: (
|
|
timeSinceLastSync /
|
|
(1000 * 60 * 60)
|
|
).toFixed(2),
|
|
nextSyncInHours: (
|
|
(FORCE_SYNC_INTERVAL - timeSinceLastSync) /
|
|
(1000 * 60 * 60)
|
|
).toFixed(2),
|
|
},
|
|
});
|
|
}
|
|
break;
|
|
|
|
case "location":
|
|
// Validate location parameters
|
|
if (!params || typeof params !== "object") {
|
|
geolocBgLogger.warn("Invalid location params", { params });
|
|
break;
|
|
}
|
|
|
|
Sentry.addBreadcrumb({
|
|
message: "Location update received",
|
|
category: "headless-task",
|
|
level: "info",
|
|
data: {
|
|
coords: params.location?.coords,
|
|
activity: params.location?.activity,
|
|
hasLocation: !!params.location,
|
|
},
|
|
});
|
|
|
|
geolocBgLogger.debug("Location update received", {
|
|
location: params.location,
|
|
});
|
|
break;
|
|
|
|
case "http":
|
|
// Validate HTTP parameters
|
|
if (!params || typeof params !== "object" || !params.response) {
|
|
geolocBgLogger.warn("Invalid HTTP params", { params });
|
|
break;
|
|
}
|
|
|
|
const httpStatus = params.response?.status;
|
|
const isHttpSuccess = httpStatus === 200;
|
|
|
|
Sentry.addBreadcrumb({
|
|
message: "HTTP response received",
|
|
category: "headless-task",
|
|
level: isHttpSuccess ? "info" : "warning",
|
|
data: {
|
|
status: httpStatus,
|
|
success: params.response?.success,
|
|
hasResponse: !!params.response,
|
|
},
|
|
});
|
|
|
|
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",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
Sentry.addBreadcrumb({
|
|
message: "Unknown event type",
|
|
category: "headless-task",
|
|
level: "warning",
|
|
data: { eventName: name },
|
|
});
|
|
}
|
|
|
|
// Task completed successfully
|
|
const taskDuration = Date.now() - taskStartTime;
|
|
|
|
Sentry.addBreadcrumb({
|
|
message: "HeadlessTask completed successfully",
|
|
category: "headless-task",
|
|
level: "info",
|
|
data: {
|
|
eventName: name,
|
|
duration: taskDuration,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
const taskDuration = Date.now() - taskStartTime;
|
|
|
|
// Capture any unexpected errors
|
|
Sentry.captureException(error, {
|
|
tags: {
|
|
module: "headless-task",
|
|
eventName: event?.name || "unknown",
|
|
},
|
|
extra: {
|
|
duration: taskDuration,
|
|
},
|
|
});
|
|
|
|
geolocBgLogger.error("HeadlessTask error", {
|
|
error,
|
|
event,
|
|
duration: taskDuration,
|
|
});
|
|
} finally {
|
|
// Clear the timeout
|
|
clearTimeout(taskTimeout);
|
|
|
|
const finalDuration = Date.now() - taskStartTime;
|
|
geolocBgLogger.debug("HeadlessTask completed", {
|
|
event: event?.name,
|
|
duration: finalDuration,
|
|
});
|
|
}
|
|
};
|
|
|
|
BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
|