Compare commits
2 commits
d780fb4190
...
4a0f3ab7ef
Author | SHA1 | Date | |
---|---|---|---|
4a0f3ab7ef | |||
8ba4056187 |
5 changed files with 266 additions and 215 deletions
|
@ -93,6 +93,7 @@
|
||||||
"@react-navigation/native": "^6.0.8",
|
"@react-navigation/native": "^6.0.8",
|
||||||
"@react-navigation/stack": "^6.3.21",
|
"@react-navigation/stack": "^6.3.21",
|
||||||
"@sentry/react-native": "~6.10.0",
|
"@sentry/react-native": "~6.10.0",
|
||||||
|
"@sentry/tracing": "^7.120.3",
|
||||||
"@turf/along": "^7.1.0",
|
"@turf/along": "^7.1.0",
|
||||||
"@turf/boolean-equal": "^7.1.0",
|
"@turf/boolean-equal": "^7.1.0",
|
||||||
"@turf/distance": "^7.1.0",
|
"@turf/distance": "^7.1.0",
|
||||||
|
|
|
@ -346,65 +346,67 @@ export default async function trackLocation() {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
locationLogger.info(`Found ${count} pending records, forcing sync`);
|
locationLogger.info(`Found ${count} pending records, forcing sync`);
|
||||||
|
|
||||||
const transaction = Sentry.startTransaction({
|
await Sentry.startSpan(
|
||||||
name: "force-sync-pending-records",
|
{
|
||||||
op: "geolocation-sync",
|
name: "force-sync-pending-records",
|
||||||
});
|
op: "geolocation-sync",
|
||||||
|
},
|
||||||
|
async (span) => {
|
||||||
|
try {
|
||||||
|
const { userToken } = getAuthState();
|
||||||
|
const state = await BackgroundGeolocation.getState();
|
||||||
|
if (userToken && state.enabled) {
|
||||||
|
const records = await BackgroundGeolocation.sync();
|
||||||
|
locationLogger.debug("Forced sync result", {
|
||||||
|
recordsCount: records?.length || 0,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
Sentry.addBreadcrumb({
|
||||||
const { userToken } = getAuthState();
|
message: "Forced sync completed",
|
||||||
const state = await BackgroundGeolocation.getState();
|
category: "geolocation",
|
||||||
if (userToken && state.enabled) {
|
level: "info",
|
||||||
const records = await BackgroundGeolocation.sync();
|
data: {
|
||||||
locationLogger.debug("Forced sync result", {
|
recordsCount: records?.length || 0,
|
||||||
recordsCount: records?.length || 0,
|
hadToken: true,
|
||||||
});
|
wasEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
span.setStatus("ok");
|
||||||
message: "Forced sync completed",
|
} else {
|
||||||
category: "geolocation",
|
Sentry.addBreadcrumb({
|
||||||
level: "info",
|
message: "Forced sync skipped",
|
||||||
data: {
|
category: "geolocation",
|
||||||
recordsCount: records?.length || 0,
|
level: "warning",
|
||||||
hadToken: true,
|
data: {
|
||||||
wasEnabled: true,
|
hasToken: !!userToken,
|
||||||
},
|
isEnabled: state.enabled,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
transaction.setStatus("ok");
|
span.setStatus("cancelled");
|
||||||
} else {
|
}
|
||||||
Sentry.addBreadcrumb({
|
} catch (error) {
|
||||||
message: "Forced sync skipped",
|
locationLogger.error("Forced sync failed", {
|
||||||
category: "geolocation",
|
error: error,
|
||||||
level: "warning",
|
stack: error.stack,
|
||||||
data: {
|
});
|
||||||
hasToken: !!userToken,
|
|
||||||
isEnabled: state.enabled,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
transaction.setStatus("cancelled");
|
Sentry.captureException(error, {
|
||||||
}
|
tags: {
|
||||||
} catch (error) {
|
module: "track-location",
|
||||||
locationLogger.error("Forced sync failed", {
|
operation: "force-sync-pending",
|
||||||
error: error,
|
},
|
||||||
stack: error.stack,
|
contexts: {
|
||||||
});
|
pendingRecords: { count },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Sentry.captureException(error, {
|
span.setStatus("internal_error");
|
||||||
tags: {
|
throw error; // Re-throw to ensure span captures the error
|
||||||
module: "track-location",
|
}
|
||||||
operation: "force-sync-pending",
|
},
|
||||||
},
|
);
|
||||||
contexts: {
|
|
||||||
pendingRecords: { count },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
transaction.setStatus("internal_error");
|
|
||||||
} finally {
|
|
||||||
transaction.finish();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
locationLogger.error("Failed to get pending records count", {
|
locationLogger.error("Failed to get pending records count", {
|
||||||
|
|
|
@ -12,176 +12,176 @@ const logger = createLogger({
|
||||||
|
|
||||||
// Background task to cancel expired notifications
|
// Background task to cancel expired notifications
|
||||||
const backgroundTask = async () => {
|
const backgroundTask = async () => {
|
||||||
const transaction = Sentry.startTransaction({
|
await Sentry.startSpan(
|
||||||
name: "auto-cancel-expired-notifications",
|
{
|
||||||
op: "background-task",
|
name: "auto-cancel-expired-notifications",
|
||||||
});
|
op: "background-task",
|
||||||
|
},
|
||||||
Sentry.getCurrentScope().setSpan(transaction);
|
async (span) => {
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info("Starting auto-cancel expired notifications task");
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Auto-cancel task started",
|
|
||||||
category: "notifications",
|
|
||||||
level: "info",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get displayed notifications with timeout protection
|
|
||||||
const getNotificationsSpan = transaction.startChild({
|
|
||||||
op: "get-displayed-notifications",
|
|
||||||
description: "Getting displayed notifications",
|
|
||||||
});
|
|
||||||
|
|
||||||
let notifications;
|
|
||||||
try {
|
|
||||||
// Add timeout protection for the API call
|
|
||||||
notifications = await Promise.race([
|
|
||||||
notifee.getDisplayedNotifications(),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(
|
|
||||||
() => reject(new Error("Timeout getting notifications")),
|
|
||||||
10000,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
getNotificationsSpan.setStatus("ok");
|
|
||||||
} catch (error) {
|
|
||||||
getNotificationsSpan.setStatus("internal_error");
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
getNotificationsSpan.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(notifications)) {
|
|
||||||
logger.warn("No notifications array received", { notifications });
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "No notifications array received",
|
|
||||||
category: "notifications",
|
|
||||||
level: "warning",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentTime = Math.round(new Date() / 1000);
|
|
||||||
let cancelledCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
logger.info("Processing notifications", {
|
|
||||||
totalNotifications: notifications.length,
|
|
||||||
currentTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Processing notifications",
|
|
||||||
category: "notifications",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
totalNotifications: notifications.length,
|
|
||||||
currentTime,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process notifications with individual error handling
|
|
||||||
for (const notification of notifications) {
|
|
||||||
try {
|
try {
|
||||||
if (!notification || !notification.id) {
|
logger.info("Starting auto-cancel expired notifications task");
|
||||||
logger.warn("Invalid notification object", { notification });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const expires = notification.data?.expires;
|
Sentry.addBreadcrumb({
|
||||||
if (!expires) {
|
message: "Auto-cancel task started",
|
||||||
continue; // Skip notifications without expiry
|
category: "notifications",
|
||||||
}
|
level: "info",
|
||||||
|
|
||||||
if (typeof expires !== "number" || expires < currentTime) {
|
|
||||||
logger.debug("Cancelling expired notification", {
|
|
||||||
notificationId: notification.id,
|
|
||||||
expires,
|
|
||||||
currentTime,
|
|
||||||
expired: expires < currentTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cancel notification with timeout protection
|
|
||||||
await Promise.race([
|
|
||||||
notifee.cancelNotification(notification.id),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(
|
|
||||||
() => reject(new Error("Timeout cancelling notification")),
|
|
||||||
5000,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
cancelledCount++;
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Notification cancelled",
|
|
||||||
category: "notifications",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
notificationId: notification.id,
|
|
||||||
expires,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (notificationError) {
|
|
||||||
errorCount++;
|
|
||||||
logger.error("Failed to process notification", {
|
|
||||||
error: notificationError,
|
|
||||||
notificationId: notification?.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Sentry.captureException(notificationError, {
|
// Get displayed notifications with timeout protection
|
||||||
|
let notifications;
|
||||||
|
await Sentry.startSpan(
|
||||||
|
{
|
||||||
|
op: "get-displayed-notifications",
|
||||||
|
description: "Getting displayed notifications",
|
||||||
|
},
|
||||||
|
async (getNotificationsSpan) => {
|
||||||
|
try {
|
||||||
|
// Add timeout protection for the API call
|
||||||
|
notifications = await Promise.race([
|
||||||
|
notifee.getDisplayedNotifications(),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error("Timeout getting notifications")),
|
||||||
|
10000,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
getNotificationsSpan.setStatus("ok");
|
||||||
|
} catch (error) {
|
||||||
|
getNotificationsSpan.setStatus("internal_error");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!Array.isArray(notifications)) {
|
||||||
|
logger.warn("No notifications array received", { notifications });
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "No notifications array received",
|
||||||
|
category: "notifications",
|
||||||
|
level: "warning",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTime = Math.round(new Date() / 1000);
|
||||||
|
let cancelledCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
logger.info("Processing notifications", {
|
||||||
|
totalNotifications: notifications.length,
|
||||||
|
currentTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "Processing notifications",
|
||||||
|
category: "notifications",
|
||||||
|
level: "info",
|
||||||
|
data: {
|
||||||
|
totalNotifications: notifications.length,
|
||||||
|
currentTime,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process notifications with individual error handling
|
||||||
|
for (const notification of notifications) {
|
||||||
|
try {
|
||||||
|
if (!notification || !notification.id) {
|
||||||
|
logger.warn("Invalid notification object", { notification });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expires = notification.data?.expires;
|
||||||
|
if (!expires) {
|
||||||
|
continue; // Skip notifications without expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof expires !== "number" || expires < currentTime) {
|
||||||
|
logger.debug("Cancelling expired notification", {
|
||||||
|
notificationId: notification.id,
|
||||||
|
expires,
|
||||||
|
currentTime,
|
||||||
|
expired: expires < currentTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel notification with timeout protection
|
||||||
|
await Promise.race([
|
||||||
|
notifee.cancelNotification(notification.id),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error("Timeout cancelling notification")),
|
||||||
|
5000,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
cancelledCount++;
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "Notification cancelled",
|
||||||
|
category: "notifications",
|
||||||
|
level: "info",
|
||||||
|
data: {
|
||||||
|
notificationId: notification.id,
|
||||||
|
expires,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (notificationError) {
|
||||||
|
errorCount++;
|
||||||
|
logger.error("Failed to process notification", {
|
||||||
|
error: notificationError,
|
||||||
|
notificationId: notification?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
Sentry.captureException(notificationError, {
|
||||||
|
tags: {
|
||||||
|
module: "auto-cancel-expired",
|
||||||
|
operation: "cancel-notification",
|
||||||
|
},
|
||||||
|
contexts: {
|
||||||
|
notification: {
|
||||||
|
id: notification?.id,
|
||||||
|
expires: notification?.data?.expires,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Auto-cancel task completed", {
|
||||||
|
totalNotifications: notifications.length,
|
||||||
|
cancelledCount,
|
||||||
|
errorCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "Auto-cancel task completed",
|
||||||
|
category: "notifications",
|
||||||
|
level: "info",
|
||||||
|
data: {
|
||||||
|
totalNotifications: notifications.length,
|
||||||
|
cancelledCount,
|
||||||
|
errorCount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
span.setStatus("ok");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Auto-cancel task failed", { error });
|
||||||
|
|
||||||
|
Sentry.captureException(error, {
|
||||||
tags: {
|
tags: {
|
||||||
module: "auto-cancel-expired",
|
module: "auto-cancel-expired",
|
||||||
operation: "cancel-notification",
|
operation: "background-task",
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
notification: {
|
|
||||||
id: notification?.id,
|
|
||||||
expires: notification?.data?.expires,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
span.setStatus("internal_error");
|
||||||
|
throw error; // Re-throw to be handled by caller
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
);
|
||||||
logger.info("Auto-cancel task completed", {
|
|
||||||
totalNotifications: notifications.length,
|
|
||||||
cancelledCount,
|
|
||||||
errorCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Auto-cancel task completed",
|
|
||||||
category: "notifications",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
totalNotifications: notifications.length,
|
|
||||||
cancelledCount,
|
|
||||||
errorCount,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
transaction.setStatus("ok");
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Auto-cancel task failed", { error });
|
|
||||||
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "background-task",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
transaction.setStatus("internal_error");
|
|
||||||
throw error; // Re-throw to be handled by caller
|
|
||||||
} finally {
|
|
||||||
transaction.finish();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAutoCancelExpired = () => {
|
export const useAutoCancelExpired = () => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as Sentry from "@sentry/react-native";
|
import * as Sentry from "@sentry/react-native";
|
||||||
|
import "@sentry/tracing";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
|
|
47
yarn.lock
47
yarn.lock
|
@ -5735,6 +5735,17 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@sentry-internal/tracing@npm:7.120.3":
|
||||||
|
version: 7.120.3
|
||||||
|
resolution: "@sentry-internal/tracing@npm:7.120.3"
|
||||||
|
dependencies:
|
||||||
|
"@sentry/core": "npm:7.120.3"
|
||||||
|
"@sentry/types": "npm:7.120.3"
|
||||||
|
"@sentry/utils": "npm:7.120.3"
|
||||||
|
checksum: 10/bd6adcced941c651596de9b2c8a35f1492c5557bda36c3283b0ef0386e72586481d4288704d0cc71eb78fd2675715488ebc4239e001a571abc44dd3363022401
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@sentry/babel-plugin-component-annotate@npm:3.2.2":
|
"@sentry/babel-plugin-component-annotate@npm:3.2.2":
|
||||||
version: 3.2.2
|
version: 3.2.2
|
||||||
resolution: "@sentry/babel-plugin-component-annotate@npm:3.2.2"
|
resolution: "@sentry/babel-plugin-component-annotate@npm:3.2.2"
|
||||||
|
@ -5841,6 +5852,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@sentry/core@npm:7.120.3":
|
||||||
|
version: 7.120.3
|
||||||
|
resolution: "@sentry/core@npm:7.120.3"
|
||||||
|
dependencies:
|
||||||
|
"@sentry/types": "npm:7.120.3"
|
||||||
|
"@sentry/utils": "npm:7.120.3"
|
||||||
|
checksum: 10/fee971b8e0bbb5b499cd1161e18c6495f9d5472c286f5de5f84dc183dcfa739d31b7b57379f1fa01eb02b67f55c8ec008c1bcdb4f8da75144efd700592099602
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@sentry/core@npm:8.54.0":
|
"@sentry/core@npm:8.54.0":
|
||||||
version: 8.54.0
|
version: 8.54.0
|
||||||
resolution: "@sentry/core@npm:8.54.0"
|
resolution: "@sentry/core@npm:8.54.0"
|
||||||
|
@ -5885,6 +5906,22 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@sentry/tracing@npm:^7.120.3":
|
||||||
|
version: 7.120.3
|
||||||
|
resolution: "@sentry/tracing@npm:7.120.3"
|
||||||
|
dependencies:
|
||||||
|
"@sentry-internal/tracing": "npm:7.120.3"
|
||||||
|
checksum: 10/6d5e673a5cd4276bd717392d5da92c9977058a2b7a6d732718b16f088a335b8c4ab8a29662781cb658010bdcac4191950cc87edf4e3fd805b48626ed2afb8994
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@sentry/types@npm:7.120.3":
|
||||||
|
version: 7.120.3
|
||||||
|
resolution: "@sentry/types@npm:7.120.3"
|
||||||
|
checksum: 10/56b9f32393b506e5e7250713fd764d755decae827ee545399dc66653eff2ddeb2f03a9c98ba5a0a846546dc37ab3af8d3535cf57ed01d9a7d00cd9dc72a55a36
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@sentry/types@npm:8.54.0":
|
"@sentry/types@npm:8.54.0":
|
||||||
version: 8.54.0
|
version: 8.54.0
|
||||||
resolution: "@sentry/types@npm:8.54.0"
|
resolution: "@sentry/types@npm:8.54.0"
|
||||||
|
@ -5894,6 +5931,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@sentry/utils@npm:7.120.3":
|
||||||
|
version: 7.120.3
|
||||||
|
resolution: "@sentry/utils@npm:7.120.3"
|
||||||
|
dependencies:
|
||||||
|
"@sentry/types": "npm:7.120.3"
|
||||||
|
checksum: 10/c50fa4b7334898c0db7840899b2fd1da1bc47a097ecbc433bc835b6e90d3e76b1761ef926cd5e9f0c15e9b00c1f091dd763862f4c98468f9628214be83fe5426
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@sentry/utils@npm:8.54.0":
|
"@sentry/utils@npm:8.54.0":
|
||||||
version: 8.54.0
|
version: 8.54.0
|
||||||
resolution: "@sentry/utils@npm:8.54.0"
|
resolution: "@sentry/utils@npm:8.54.0"
|
||||||
|
@ -6883,6 +6929,7 @@ __metadata:
|
||||||
"@react-navigation/native": "npm:^6.0.8"
|
"@react-navigation/native": "npm:^6.0.8"
|
||||||
"@react-navigation/stack": "npm:^6.3.21"
|
"@react-navigation/stack": "npm:^6.3.21"
|
||||||
"@sentry/react-native": "npm:~6.10.0"
|
"@sentry/react-native": "npm:~6.10.0"
|
||||||
|
"@sentry/tracing": "npm:^7.120.3"
|
||||||
"@turf/along": "npm:^7.1.0"
|
"@turf/along": "npm:^7.1.0"
|
||||||
"@turf/boolean-equal": "npm:^7.1.0"
|
"@turf/boolean-equal": "npm:^7.1.0"
|
||||||
"@turf/distance": "npm:^7.1.0"
|
"@turf/distance": "npm:^7.1.0"
|
||||||
|
|
Loading…
Add table
Reference in a new issue