Spaces:
Running
Running
| 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, | |
| } | |
| ); | |
| }); | |
| } | |