File size: 3,966 Bytes
fc288c8
 
 
be9292f
 
 
 
fc288c8
 
be9292f
 
 
 
 
 
 
 
 
 
fc288c8
be9292f
 
fc288c8
be9292f
 
 
 
 
fc288c8
be9292f
 
 
93a912e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fc288c8
 
93a912e
fc288c8
f0abe95
fc288c8
be9292f
fc288c8
be9292f
fc288c8
 
 
 
be9292f
fc288c8
be9292f
 
93a912e
f0abe95
 
93a912e
fc288c8
93a912e
f0abe95
93a912e
be9292f
 
fc288c8
 
 
 
 
be9292f
fc288c8
be9292f
 
 
fc288c8
 
be9292f
fc288c8
be9292f
 
 
 
 
 
 
 
 
 
 
fc288c8
be9292f
 
 
 
 
fc288c8
be9292f
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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,
      }
    );
  });
}