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
JavaScript
;
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