UNPKG

@jillen/analytics

Version:

Advanced analytics package for Next.js applications with bot detection and comprehensive tracking

303 lines (298 loc) 9.71 kB
'use strict'; var react = require('react'); var navigation = require('next/navigation'); var isbot = require('isbot'); // visitor-tracker.tsx // analytics-constants.ts var ANALYTICS_CONFIG = { SERVER_URL: process.env.NEXT_PUBLIC_ANALYTICS_SERVER_URL || "", SITE_ID: process.env.NEXT_PUBLIC_ANALYTICS_SITE_ID || "" }; function validateAnalyticsConfig() { const { SERVER_URL, SITE_ID } = ANALYTICS_CONFIG; if (!SERVER_URL || !SITE_ID) { console.warn("[Analytics] Missing environment variables:", { SERVER_URL: SERVER_URL ? "\u2713" : "\u2717 NEXT_PUBLIC_ANALYTICS_SERVER_URL", SITE_ID: SITE_ID ? "\u2713" : "\u2717 NEXT_PUBLIC_ANALYTICS_SITE_ID" }); return false; } return true; } // visitor-tracker.tsx function VisitorTracker({ ip, country, city, region, continent, latitude, longitude, timezone, postalCode, host, protocol, deploymentUrl, route, userAgent, edgeRegion, cacheStatus }) { const pathname = navigation.usePathname(); const isInitialized = react.useRef(false); const lastTrackedPath = react.useRef(route); const checkIfBot = react.useCallback(() => { if (isbot.isbot(userAgent)) { return true; } if (typeof navigator !== "undefined" && isbot.isbot(navigator.userAgent)) { return true; } if (typeof window !== "undefined") { const win = window; if (win.navigator?.webdriver || win.phantom || win.callPhantom || win.__nightmare) { return true; } if (!window.localStorage || !window.sessionStorage) { return true; } } return false; }, [userAgent]); const generateVisitorId = (ip2, userAgent2) => { const normalizedUA = userAgent2.toLowerCase().replace(/\s+/g, ""); const combined = `${ip2}:${normalizedUA}`; return btoa(combined).replace(/[^a-zA-Z0-9]/g, "").substring(0, 16); }; const generateSessionId = () => { if (typeof window !== "undefined") { const existingSessionId = sessionStorage.getItem("analytics_session_id"); if (existingSessionId) { return existingSessionId; } } const generateLightweightId = () => { try { const array = new Uint8Array(4); crypto.getRandomValues(array); const timestamp = Date.now().toString(36).slice(-4); const randomPart = Array.from(array).map((byte) => byte.toString(36)).join("").slice(0, 4); return `${timestamp}${randomPart}`.substring(0, 8); } catch { const timestamp = Date.now().toString(36).slice(-4); const randomPart = Math.random().toString(36).substring(2, 6); return `${timestamp}${randomPart}`.substring(0, 8); } }; const newSessionId = generateLightweightId(); if (typeof window !== "undefined") { try { sessionStorage.setItem("analytics_session_id", newSessionId); } catch { } } return newSessionId; }; const getClientData = react.useCallback(() => { if (typeof window === "undefined") { return { isNewVisitor: true, screenResolution: null, viewportSize: null, connectionType: null, clientTimeZone: null, sessionStartTime: (/* @__PURE__ */ new Date()).toISOString() }; } const visitorId = generateVisitorId(ip, userAgent); const sessionId = generateSessionId(); const sessionCacheKey = `isNewVisitor_${sessionId}`; const cachedIsNewVisitor = sessionStorage.getItem(sessionCacheKey); let isNewVisitor; if (cachedIsNewVisitor === null) { const visitorExists = localStorage.getItem( `analytics_visitor_${visitorId}` ); isNewVisitor = !visitorExists; sessionStorage.setItem(sessionCacheKey, isNewVisitor.toString()); if (isNewVisitor) { localStorage.setItem( `analytics_visitor_${visitorId}`, Date.now().toString() ); } } else { isNewVisitor = cachedIsNewVisitor === "true"; } let sessionStartTime = localStorage.getItem( `analytics_session_start_${sessionId}` ); if (!sessionStartTime) { sessionStartTime = (/* @__PURE__ */ new Date()).toISOString(); localStorage.setItem( `analytics_session_start_${sessionId}`, sessionStartTime ); } const screenResolution = `${screen.width}x${screen.height}`; const viewportSize = `${window.innerWidth}x${window.innerHeight}`; const connectionType = (() => { try { const nav = navigator; return nav.connection?.effectiveType || nav.connection?.type || "unknown"; } catch { return "unknown"; } })(); const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; return { isNewVisitor, screenResolution, viewportSize, connectionType, clientTimeZone, sessionStartTime }; }, [ip, userAgent]); const sendAnalyticsEvent = react.useCallback( async (eventType, referrer, customPath) => { if (process.env.NODE_ENV !== "production" && false) ; if (!validateAnalyticsConfig()) { return; } const serverUrl = ANALYTICS_CONFIG.SERVER_URL; const siteId = ANALYTICS_CONFIG.SITE_ID; const endpoint = `${serverUrl}/api/log/ingest`; const language = typeof navigator !== "undefined" ? navigator.language || navigator.languages?.[0] || void 0 : void 0; const doNotTrack = typeof navigator !== "undefined" ? navigator.doNotTrack === "1" : false; const isMobile = typeof navigator !== "undefined" ? /Mobi|Android/i.test(navigator.userAgent) : /Mobi|Android/i.test(userAgent); const clientData = getClientData(); const isDev = process.env.NODE_ENV === "development"; const devCountry = isDev && !country ? "US" : country; const devCity = isDev && !city ? "San Francisco" : city; const devRegion = isDev && !region ? "CA" : region; const devEdgeRegion = isDev && !edgeRegion ? "sfo1" : edgeRegion; try { const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, mode: "cors", body: JSON.stringify({ siteId, path: customPath || route, visitorId: generateVisitorId(ip, userAgent), sessionId: generateSessionId(), eventType, // Enhanced client-side fields isNewVisitor: clientData.isNewVisitor, screenResolution: clientData.screenResolution, viewportSize: clientData.viewportSize, connectionType: clientData.connectionType, clientTimeZone: clientData.clientTimeZone, sessionStartTime: clientData.sessionStartTime, // Server-side fields ipAddress: ip, userAgent, referrer, country: devCountry || void 0, city: devCity || void 0, region: devRegion || void 0, continent: continent || void 0, latitude: latitude ? parseFloat(latitude) : void 0, longitude: longitude ? parseFloat(longitude) : void 0, timezone: timezone || void 0, postalCode: postalCode || void 0, host: host || void 0, protocol: protocol || void 0, deploymentUrl: deploymentUrl || void 0, edgeRegion: devEdgeRegion || void 0, language, doNotTrack, isMobile, cacheStatus: cacheStatus || "UNKNOWN" }) }); if (!response.ok) { const error = await response.json(); console.error("[Analytics] Failed to track event:", error); } } catch (error) { console.error("[Analytics] Tracking failed:", error); } }, [ ip, country, city, region, continent, latitude, longitude, timezone, postalCode, host, protocol, deploymentUrl, route, userAgent, edgeRegion, cacheStatus, getClientData ] ); react.useEffect(() => { if (typeof window === "undefined") return; const isProduction = process.env.NEXT_PUBLIC_VERCEL_ENV === "production" || process.env.NODE_ENV === "production" && !process.env.NEXT_PUBLIC_VERCEL_ENV; if (!isProduction) { return; } if (checkIfBot()) { return; } const currentPath = pathname || route; if (!isInitialized.current) { isInitialized.current = true; const sessionId = generateSessionId(); const sessionKey = `analytics_session_active_${sessionId}`; const isSessionActive = localStorage.getItem(sessionKey); if (!isSessionActive) { sendAnalyticsEvent("session_start", void 0, currentPath); localStorage.setItem(sessionKey, "true"); } const referrer = document.referrer && document.referrer.length > 0 ? document.referrer : void 0; sendAnalyticsEvent("pageview", referrer, currentPath); lastTrackedPath.current = currentPath; } else if (currentPath !== lastTrackedPath.current) { sendAnalyticsEvent("pageview", void 0, currentPath); lastTrackedPath.current = currentPath; } return; }, [ pathname, route, ip, country, city, region, continent, latitude, longitude, timezone, postalCode, host, protocol, deploymentUrl, userAgent, edgeRegion, cacheStatus, sendAnalyticsEvent, getClientData, checkIfBot ]); return null; } exports.VisitorTracker = VisitorTracker; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map