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