3dgs_measurement_tool / client /src /utils /visitorTracking.js
dengdeyan's picture
visilog
93a912e
import { getApiUrl } from "./apiConfig"; // Assuming this correctly returns the server's base URL
// --- Constants ---
const VISITOR_ID_KEY = "gda.visitorId";
const SESSION_MARK_KEY = "gda.visitRecorded";
const LOCATION_CACHE_KEY = "gda.coarseLocation";
// --- Core Helper Functions (These are well-written and retained) ---
function generateVisitorId() {
if (typeof crypto !== "undefined" && crypto.randomUUID) {
return crypto.randomUUID();
}
const random = Math.random().toString(36).slice(2);
const time = Date.now().toString(36);
return `v_${time}_${random}`;
}
function getOrCreateVisitorId() {
if (typeof window === "undefined") return null;
try {
const existing = localStorage.getItem(VISITOR_ID_KEY);
if (existing) return existing;
const id = generateVisitorId();
localStorage.setItem(VISITOR_ID_KEY, id);
return id;
} catch {
return generateVisitorId(); // Fallback for environments where localStorage might fail
}
}
/**
* A generic function to send any tracking event to the server.
* This aligns with the server's `/api/track` endpoint and Pydantic model.
* @param {string} eventType - The type of event (e.g., 'page_view', 'file_upload_start').
* @param {object} eventData - A dictionary of additional data related to the event.
*/
export async function trackEvent(eventType, eventData = {}) {
if (typeof window === "undefined") return;
const clientId = getOrCreateVisitorId();
if (!clientId) return;
const payload = {
client_id: clientId,
event_type: eventType,
event_data: eventData,
};
// This is a "fire-and-forget" request. We don't wait for the response,
// and we use `keepalive` to ensure the request is sent even if the user
// is navigating away from the page.
try {
fetch(getApiUrl("/api/track"), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
keepalive: true,
});
} catch (error) {
console.warn("Could not send tracking event to server:", error);
}
}
/**
* Records a single page visit per session to the server.
* This now uses the generic `trackEvent` function.
*/
export async function recordVisit() {
if (typeof window === "undefined") return;
// Use sessionStorage to ensure this only runs once per browser session.
try {
if (sessionStorage.getItem(SESSION_MARK_KEY)) {
return;
}
sessionStorage.setItem(SESSION_MARK_KEY, "1");
} catch {
// If sessionStorage fails, proceed anyway but might record multiple visits.
}
// Get location data to include in the event
const location = await getCoarseLocation();
trackEvent('page_view', {
path: `${window.location.pathname}${window.location.search}${window.location.hash}`,
referrer: document.referrer,
location: location,
});
}
/**
* Fetches the user's coarse location using the Geolocation API.
* This is well-written and retained.
* @returns {Promise<object|null>}
*/
export function getCoarseLocation() {
if (typeof window === "undefined") return Promise.resolve(null);
try {
const cached = sessionStorage.getItem(LOCATION_CACHE_KEY);
if (cached) return Promise.resolve(JSON.parse(cached));
} catch { /* Ignore cache errors */ }
if (!("geolocation" in navigator)) return Promise.resolve(null);
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(
(position) => {
const location = {
lat: position.coords.latitude,
lon: position.coords.longitude,
accuracyMeters: position.coords.accuracy,
};
try {
sessionStorage.setItem(LOCATION_CACHE_KEY, JSON.stringify(location));
} catch { /* Ignore cache errors */ }
resolve(location);
},
() => resolve(null),
{
enableHighAccuracy: false,
maximumAge: 10 * 60 * 1000, // 10 minutes
timeout: 8000,
}
);
});
}