UNPKG

bear-tracker

Version:

Lightweight bot detection middleware for tracking AI crawler visits (OpenAI, ChatGPT, etc.) with API support and analytics

214 lines 9.42 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BotTracker = void 0; const bot_patterns_1 = require("./bot-patterns"); const logger_1 = require("./logger"); class BotTracker { constructor(options = {}) { this.options = { enableLogging: options.enableLogging ?? true, customLogger: options.customLogger ?? null, trackOnlyBots: options.trackOnlyBots ?? false, includeIp: options.includeIp ?? true, logLevel: options.logLevel ?? 'info' }; this.logger = new logger_1.BotLogger(this.options.logLevel); } middleware() { return (req, res, next) => { const userAgent = req.headers['user-agent'] || ''; const detection = (0, bot_patterns_1.detectBot)(userAgent); // Extract IP address let ip; if (this.options.includeIp) { ip = req.ip || req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || req.connection?.remoteAddress || undefined; } const botInfo = { name: detection.name, type: detection.type, isBot: detection.isBot, userAgent, timestamp: new Date(), ...(ip && { ip }), ...(detection.description && { description: detection.description }) }; // Add bot info to response locals for downstream middleware if (res.locals) { res.locals.botInfo = botInfo; } // Log based on options if (this.options.enableLogging) { if (!this.options.trackOnlyBots || botInfo.isBot) { if (this.options.customLogger) { this.options.customLogger(botInfo); } else { this.logger.log(botInfo); } } } next(); }; } static createQuickTracker(logLevel = 'info') { const tracker = new BotTracker({ logLevel, trackOnlyBots: true }); return tracker.middleware(); } static createCustomTracker(customLogger) { const tracker = new BotTracker({ customLogger, enableLogging: true }); return tracker.middleware(); } // NEW: Simple API tracker for Next.js/Vercel with API key support static createApiTracker(apiUrl, apiKey, logLevel = 'warn') { if (!apiKey) { throw new Error('API key is required for createApiTracker'); } return async (request, response, next) => { const userAgent = request.headers?.get?.('user-agent') || request.headers?.['user-agent'] || ''; const ip = request.ip || request.headers?.get?.('x-forwarded-for')?.split(',')[0]?.trim() || request.headers?.['x-forwarded-for']?.split(',')[0]?.trim(); const website = request.nextUrl?.hostname || request.headers?.host || 'unknown'; const detection = (0, bot_patterns_1.detectBot)(userAgent); if (detection.isBot) { // Log locally const logMessage = `🤖 ${detection.name} detected (${detection.type})`; if (logLevel === 'warn') console.warn(logMessage); else if (logLevel === 'error') console.error(logMessage); else console.log(logMessage); // Send to API with API key (fire-and-forget) fetch(`${apiUrl}/api/track`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userAgent, ip, website, apiKey }) }).catch(() => { }); // Silent fail } // Handle different middleware patterns if (next) { return next(); // Express-style } else if (response) { return response.next(); // Some middleware patterns } else { // Next.js middleware - return NextResponse const { NextResponse } = require('next/server'); return NextResponse.next(); } }; } // NEW: Next.js specific middleware (proper async function) static createNextMiddleware(apiUrl, apiKey) { if (!apiKey) { throw new Error('API key is required for createNextMiddleware'); } return async (request) => { const userAgent = request.headers?.get?.('user-agent') || ''; const referrer = request.headers?.get?.('referer') || request.headers?.get?.('referrer') || undefined; // Extract request details const ip = request.ip || request.headers?.get?.('x-forwarded-for')?.split(',')[0]?.trim(); const website = request.nextUrl?.hostname || 'unknown'; const page = request.nextUrl?.pathname || '/'; // Get full URL with parameters for ChatGPT referral detection // Try multiple ways to access URL search parameters let fullUrl = page; let hasUtmParams = false; try { // Next.js specific: try to access searchParams const searchParams = request.nextUrl?.searchParams; if (searchParams) { const utmSource = searchParams.get('utm_source'); if (utmSource) { hasUtmParams = true; fullUrl = `${page}?utm_source=${utmSource}`; // Add other params if they exist const utmMedium = searchParams.get('utm_medium'); const utmCampaign = searchParams.get('utm_campaign'); if (utmMedium) fullUrl += `&utm_medium=${utmMedium}`; if (utmCampaign) fullUrl += `&utm_campaign=${utmCampaign}`; } } // Fallback: check if request has URL string with params if (!hasUtmParams) { const requestUrl = request.url || request.href; if (requestUrl && typeof requestUrl === 'string') { hasUtmParams = requestUrl.includes('utm_source'); if (hasUtmParams) { fullUrl = requestUrl; } } } } catch (e) { // Fallback for any errors console.log('Bear Tracker: Error accessing URL params, using pathname only'); } // Check for bots first const botInfo = BotTracker.detectBotFromUserAgent(userAgent); // Always send to API if: bot detected, UTM params found, or has referrer const shouldTrack = botInfo.isBot || hasUtmParams || referrer; if (shouldTrack) { console.log(`Bear Tracker: Tracking request - Bot: ${botInfo.isBot}, UTM: ${hasUtmParams}, Referrer: ${!!referrer}`); const payload = { userAgent, ip, website, page, url: fullUrl, referrer, apiKey }; console.log(`Bear Tracker: Sending API call to ${apiUrl}/api/track`, payload); // Fire-and-forget API call with full data fetch(`${apiUrl}/api/track`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(response => { console.log(`Bear Tracker: API response status: ${response.status}`); if (!response.ok) { return response.text().then(text => { console.error(`Bear Tracker: API error ${response.status}: ${text}`); }); } return response.json().then(data => { console.log('Bear Tracker: API success:', data); }); }) .catch(error => { console.error('Bear Tracker: Fetch error:', error.message); }); } // Import NextResponse dynamically to avoid build issues try { const { NextResponse } = require('next/server'); return NextResponse.next(); } catch { // Fallback for non-Next.js environments return; } }; } static detectBotFromUserAgent(userAgent) { const detection = (0, bot_patterns_1.detectBot)(userAgent); return { name: detection.name, type: detection.type, isBot: detection.isBot, userAgent, timestamp: new Date() }; } } exports.BotTracker = BotTracker; //# sourceMappingURL=middleware.js.map