UNPKG

@jillen/analytics

Version:

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

445 lines (436 loc) 14.5 kB
import { useRef, useCallback, useEffect } from 'react'; import { usePathname } from 'next/navigation'; import { isbot } from 'isbot'; var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // 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 = usePathname(); const isInitialized = useRef(false); const lastTrackedPath = useRef(route); const checkIfBot = useCallback(() => { if (isbot(userAgent)) { return true; } if (typeof navigator !== "undefined" && 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 = 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 = 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 ] ); 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; } // server/header-parser.ts function parseAnalyticsHeaders(headers) { const ip = headers.get("x-forwarded-for") || headers.get("x-real-ip") || "unknown"; const country = headers.get("x-vercel-ip-country") || null; const userAgent = headers.get("user-agent") || ""; const city = headers.get("x-vercel-ip-city") || null; const region = headers.get("x-vercel-ip-country-region") || null; const continent = headers.get("x-vercel-ip-continent") || null; const latitude = headers.get("x-vercel-ip-latitude") || null; const longitude = headers.get("x-vercel-ip-longitude") || null; const timezone = headers.get("x-vercel-ip-timezone") || null; const postalCode = headers.get("x-vercel-ip-postal-code") || null; const host = headers.get("host") || null; const protocol = headers.get("x-forwarded-proto"); const deploymentUrl = headers.get("x-vercel-deployment-url") || null; const vercelId = headers.get("x-vercel-id") || null; const edgeRegion = vercelId ? vercelId.split("::")[0] : null; const cacheStatus = null; return { ip, country, city, region, continent, latitude, longitude, timezone, postalCode, host, protocol, deploymentUrl, userAgent, edgeRegion, cacheStatus }; } // analytics-bot-utils.ts function generateSimpleBotVisitorId(ip, userAgent) { const normalizedUA = userAgent.toLowerCase().replace(/\s+/g, ""); const combined = `bot:${ip}:${normalizedUA}`; return Buffer.from(combined).toString("base64").replace(/[^a-zA-Z0-9]/g, "").substring(0, 16); } function generateSimpleBotSessionId(ip) { const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0]; const combined = `bot-session:${ip}:${today}`; return Buffer.from(combined).toString("base64").replace(/[^a-zA-Z0-9]/g, "").substring(0, 16); } function trackBotVisit(request, pathname) { void (async () => { try { if (!validateAnalyticsConfig()) { return; } const serverUrl = ANALYTICS_CONFIG.SERVER_URL; const siteId = ANALYTICS_CONFIG.SITE_ID; const userAgent = request.headers.get("user-agent") || ""; const ip = request.headers.get("x-forwarded-for")?.split(",")[0] || request.headers.get("x-real-ip") || "unknown"; const visitorId = generateSimpleBotVisitorId(ip, userAgent); const sessionId = generateSimpleBotSessionId(ip); const botPayload = { // Required fields (minimal values) siteId, path: pathname, visitorId, sessionId, eventType: "pageview", // Essential bot data userAgent, ipAddress: ip, // Client-side fields (bots don't have these) isNewVisitor: true, screenResolution: null, viewportSize: null, connectionType: null, clientTimeZone: null, sessionStartTime: (/* @__PURE__ */ new Date()).toISOString(), // Optional server fields (skip expensive lookups) referrer: request.headers.get("referer") || null, country: null, city: null, region: null, continent: null, latitude: null, longitude: null, timezone: null, postalCode: null, host: request.headers.get("host") || null, protocol: request.headers.get("x-forwarded-proto") || null, deploymentUrl: null, edgeRegion: null, language: null, doNotTrack: false, isMobile: /Mobi|Android/i.test(userAgent), cacheStatus: "UNKNOWN" }; const endpoint = `${serverUrl}/api/log/ingest`; await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, mode: "cors", body: JSON.stringify(botPayload) }); } catch { return; } })(); } // server/middleware-utils.ts function setupAnalyticsMiddleware(request) { const pathname = request.nextUrl.pathname; const headers = new Headers(request.headers); headers.set("x-pathname", pathname); const userAgent = request.headers.get("user-agent") || ""; if (isbot(userAgent)) { trackBotVisit(request, pathname); } return { headers, pathname }; } // client/index.ts var client_exports = {}; __export(client_exports, { VisitorTracker: () => VisitorTracker }); // server/index.ts var server_exports = {}; __export(server_exports, { parseAnalyticsHeaders: () => parseAnalyticsHeaders, setupAnalyticsMiddleware: () => setupAnalyticsMiddleware, trackBotVisit: () => trackBotVisit }); export { ANALYTICS_CONFIG, VisitorTracker, client_exports as client, parseAnalyticsHeaders, server_exports as server, setupAnalyticsMiddleware, trackBotVisit, validateAnalyticsConfig }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map