@jillen/analytics
Version:
Advanced analytics package for Next.js applications with bot detection and comprehensive tracking
445 lines (436 loc) • 14.5 kB
JavaScript
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