From 60327fc659cfa843470f652c500e7b9e0a821b1e Mon Sep 17 00:00:00 2001 From: Bryan Wills Date: Sat, 2 Aug 2025 02:40:46 -0400 Subject: [PATCH 1/2] fix: Restore 24-hour banner logic - Remove temporary testing code and restore original localStorage-based logic that shows banner only once every 24 hours --- README.md | 20 ++++++++++- TODO_BLOG_FEATURES.md | 49 ++++++++++++++++++++++++++ src/components/shared/UpdateBanner.tsx | 16 ++------- 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e215bc4..e9d6d5b 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,22 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Big Brain Coding - Modern Software Development + +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app) for Big Brain Coding's company website. + +## 🚀 Features + +### Current Features: +- **Modern Web Design** - Responsive, accessible, and performant +- **AI Integration Services** - Showcase of AI-powered solutions +- **Project Portfolio** - Interactive project showcases +- **Contact Management** - Professional contact forms and communication +- **Analytics Integration** - Comprehensive tracking and insights +- **Welcome Banner** - Informative banner with 24-hour persistence + +### Coming Soon: +- **Automated Project Documentation** - Screenshot generation for GitHub repos and live websites +- **Blog System** - MDX-based content with authentication and comments +- **E-commerce Integration** - Stripe-powered payment processing +- **Advanced Analytics** - Enhanced tracking and reporting features ## Getting Started diff --git a/TODO_BLOG_FEATURES.md b/TODO_BLOG_FEATURES.md index f1e837a..c08c5a2 100755 --- a/TODO_BLOG_FEATURES.md +++ b/TODO_BLOG_FEATURES.md @@ -17,6 +17,55 @@ --- +## 🖼️ Project Screenshot Automation (Coming Soon) + +### Automated Project Documentation: +- [ ] **Screenshot Generation Script** - Puppeteer-based automation +- [ ] **GitHub Repository Integration** - Capture repo information and stats +- [ ] **Live Website Screenshots** - Capture deployed project screenshots +- [ ] **Multi-Resolution Support** - Desktop (1920x1080), tablet, and mobile views +- [ ] **Modal Gallery System** - Interactive project showcase with image carousel +- [ ] **Technology Stack Display** - Automated detection and display of tech stack +- [ ] **Project Statistics** - Stars, forks, issues, and deployment status + +### Screenshot Capture Features: +- [ ] **GitHub Repository Pages** - Main branch, README, and key files +- [ ] **Live Website Pages** - Homepage, features, about, and key functionality +- [ ] **Responsive Design Testing** - Multiple viewport sizes for comprehensive coverage +- [ ] **Custom Viewport Support** - Specific resolutions for optimal presentation +- [ ] **Error Handling** - Graceful fallbacks for unavailable sites or repos + +### Technical Implementation: +- [ ] **Puppeteer Integration** - Reliable screenshot generation with headless Chrome +- [ ] **File Storage System** - Organized screenshot storage with versioning +- [ ] **Database Integration** - Project metadata and screenshot management +- [ ] **Manual Trigger System** - On-demand screenshot generation for updates +- [ ] **Image Optimization** - Compressed screenshots for fast loading +- [ ] **Caching Strategy** - Efficient storage and retrieval of project assets + +### User Experience Features: +- [ ] **Interactive Project Modals** - Rich project showcase with multiple views +- [ ] **Image Carousel Navigation** - Smooth browsing through project screenshots +- [ ] **Technology Badges** - Visual representation of project tech stack +- [ ] **GitHub Statistics Display** - Real-time repository metrics +- [ ] **Responsive Gallery** - Mobile-friendly project presentation +- [ ] **Loading States** - Smooth user experience during image generation + +### Workflow Process: +1. **Setup Phase** - Configure screenshot scripts for each project +2. **Capture Phase** - Generate screenshots of GitHub repos and live websites +3. **Integration Phase** - Update project pages with new screenshots and data +4. **Maintenance Phase** - Re-run scripts when projects undergo major updates + +### Benefits: +- **Consistent Project Documentation** - Standardized presentation across all projects +- **Time-Saving Automation** - Eliminates manual screenshot capture and editing +- **Professional Presentation** - High-quality, consistent project showcases +- **Easy Updates** - Simple script execution for project refresh +- **Comprehensive Coverage** - Both code and live website documentation + +--- + ## 📝 Blog Implementation (Future Priority) ### Core Blog Features: diff --git a/src/components/shared/UpdateBanner.tsx b/src/components/shared/UpdateBanner.tsx index 20dc7de..1e4bea2 100644 --- a/src/components/shared/UpdateBanner.tsx +++ b/src/components/shared/UpdateBanner.tsx @@ -13,19 +13,6 @@ export default function UpdateBanner() { const [isDismissed, setIsDismissed] = useState(false) useEffect(() => { - // TEMPORARY: Force banner to appear on every page refresh for testing - // TODO: Remove this before commit/deploy and restore the original logic below - - // Show banner after delay - const timer = setTimeout(() => { - setIsVisible(true) - // Set CSS custom property for header positioning - document.documentElement.style.setProperty('--banner-height', '60px') - }, BANNER_DELAY) - - return () => clearTimeout(timer) - - /* ORIGINAL LOGIC (commented out for testing): // Check if banner was dismissed within last 24 hours const dismissedTime = localStorage.getItem(BANNER_KEY) const now = Date.now() @@ -35,11 +22,12 @@ export default function UpdateBanner() { // Show banner after delay const timer = setTimeout(() => { setIsVisible(true) + // Set CSS custom property for header positioning + document.documentElement.style.setProperty('--banner-height', '60px') }, BANNER_DELAY) return () => clearTimeout(timer) } - */ }, []) const handleDismiss = () => { From 74decf1e68d491b8cdceb456ec16fc282f3e9828 Mon Sep 17 00:00:00 2001 From: Bryan Wills Date: Sat, 2 Aug 2025 13:57:27 -0400 Subject: [PATCH 2/2] feat: Implement advanced NGINX log analytics with timezone conversion and real IP detection - Added nginxLogParser with UTC to ET timezone conversion - Fixed IP address extraction to show real visitor IPs instead of Docker container IPs - Implemented EnhancedAnalyticsDashboard with comprehensive analytics - Added marketing intelligence features and bot detection - Created new UI components (progress, select, tabs) for enhanced dashboard - Fixed date filtering logic for proper log parsing - Added comprehensive tracking and analytics API endpoints --- package.json | 2 + pnpm-lock.yaml | 76 +++ src/app/analytics/page.tsx | 310 +--------- .../analytics/marketing-intelligence/route.ts | 130 +++++ src/app/api/analytics/nginx-logs/route.ts | 109 ++++ src/app/api/tracking/route.ts | 63 +- .../analytics/EnhancedAnalyticsDashboard.tsx | 407 +++++++++++++ .../MarketingIntelligenceDashboard.tsx | 538 ++++++++++++++++++ src/components/ui/progress.tsx | 31 + src/components/ui/select.tsx | 160 ++++++ src/components/ui/tabs.tsx | 66 +++ src/lib/botDetection.ts | 208 +++++++ src/lib/marketingIntelligence.ts | 489 ++++++++++++++++ src/lib/nginxLogParser.ts | 450 +++++++++++++++ 14 files changed, 2730 insertions(+), 309 deletions(-) create mode 100644 src/app/api/analytics/marketing-intelligence/route.ts create mode 100644 src/app/api/analytics/nginx-logs/route.ts create mode 100644 src/components/analytics/EnhancedAnalyticsDashboard.tsx create mode 100644 src/components/analytics/MarketingIntelligenceDashboard.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/lib/botDetection.ts create mode 100644 src/lib/marketingIntelligence.ts create mode 100644 src/lib/nginxLogParser.ts diff --git a/package.json b/package.json index ec1273b..3161004 100755 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f7d44b..e550f84 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,12 @@ importers: '@radix-ui/react-navigation-menu': specifier: ^1.2.13 version: 1.2.13(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-progress': + specifier: ^1.1.7 + version: 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-select': + specifier: ^2.2.5 + version: 2.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.8)(react@19.1.0) @@ -842,6 +848,9 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} @@ -1150,6 +1159,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.10': resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} peerDependencies: @@ -1163,6 +1185,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.2.5': + resolution: {integrity: sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.0': resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==} peerDependencies: @@ -4241,6 +4276,8 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@radix-ui/number@1.1.1': {} + '@radix-ui/primitive@1.1.2': {} '@radix-ui/react-accordion@1.2.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': @@ -4557,6 +4594,16 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -4574,6 +4621,35 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-select@2.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.1(@types/react@19.1.8)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-slot@1.2.0(@types/react@19.1.8)(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) diff --git a/src/app/analytics/page.tsx b/src/app/analytics/page.tsx index 597d83d..61f7aaf 100755 --- a/src/app/analytics/page.tsx +++ b/src/app/analytics/page.tsx @@ -1,315 +1,11 @@ 'use client' -import { useEffect, useState } from 'react' - -interface TrackingEvent { - timestamp: string - sessionId: string - eventType: string - pageUrl: string - ipAddress: string - userAgent: string - deviceInfo: { - browser: string - deviceType: string - screenWidth: number - screenHeight: number - language: string - } - timeOnPage?: number - scrollDepth?: number -} - -interface IPSummary { - ip: string - totalVisits: number - uniqueSessions: number - pages: { [key: string]: number } - devices: { [key: string]: number } - browsers: { [key: string]: number } - averageTimeOnPage: number - lastVisit: string - firstVisit: string -} - -interface AnalyticsData { - summary: IPSummary[] - events: TrackingEvent[] - totalVisitors: number - totalSessions: number - totalPageViews: number -} +import EnhancedAnalyticsDashboard from '@/components/analytics/EnhancedAnalyticsDashboard' export default function AnalyticsPage() { - const [realIP, setRealIP] = useState('unknown') - const [allowedIP, setAllowedIP] = useState('') - const [isAllowed, setIsAllowed] = useState(false) - const [loading, setLoading] = useState(true) - const [analyticsData, setAnalyticsData] = useState(null) - const [selectedIP, setSelectedIP] = useState('') - const [dateRange, setDateRange] = useState('today') - - useEffect(() => { - // Get IP from API - fetch('/api/test-ip') - .then(res => res.json()) - .then(data => { - setRealIP(data.yourIP) - setAllowedIP(data.allowedIP) - setIsAllowed(data.isAllowed) - setLoading(false) - }) - .catch(() => { - setLoading(false) - }) - }, []) - - useEffect(() => { - if (isAllowed) { - fetchAnalyticsData() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isAllowed, dateRange]) - - const fetchAnalyticsData = async () => { - try { - // Convert dateRange to actual date - let date; - const today = new Date(); - - switch (dateRange) { - case 'today': - date = today.toISOString().split('T')[0]; - break; - case 'yesterday': - const yesterday = new Date(today); - yesterday.setDate(yesterday.getDate() - 1); - date = yesterday.toISOString().split('T')[0]; - break; - case 'week': - // For now, just use today's date - date = today.toISOString().split('T')[0]; - break; - default: - date = today.toISOString().split('T')[0]; - } - - const response = await fetch('/api/analytics/dashboard', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ date }) - }) - - if (response.ok) { - const data = await response.json() - setAnalyticsData(data) - } - } catch (error) { - console.error('Error fetching analytics data:', error) - } - } - - if (loading) { - return ( -
-
-

Loading analytics...

-
-
- ) - } - - // Check if user is accessing from allowed IP - if (!isAllowed) { - return ( -
-
-
-

Access Denied

-

- Analytics dashboard is only accessible from authorized IP addresses. -

-

- Your IP: {realIP} -

-

- Allowed IP: {allowedIP || 'Not set'} -

-
-
-
- ) - } - return ( -
-
-
-

Analytics Dashboard

-

- Real-time visitor analytics and IP-based tracking data -

-
-

- ✅ Access granted from authorized IP: {realIP} -

-
-
- - {/* Date Range Selector */} -
- - -
- - {analyticsData ? ( -
- {/* Overview Stats */} -
-
-

Total Visitors

-

{analyticsData.totalVisitors}

-
-
-

Total Sessions

-

{analyticsData.totalSessions}

-
-
-

Page Views

-

{analyticsData.totalPageViews}

-
-
-

Active IPs

-

{analyticsData.summary.length}

-
-
- - {/* IP Address Summaries */} -
-

Visitor IP Addresses

- {analyticsData.summary.length === 0 ? ( -

No visitor data found for this date range.

- ) : ( -
- {analyticsData.summary.map((ipSummary) => ( -
-
-

{ipSummary.ip}

- -
-
-
- Total Visits: -

{ipSummary.totalVisits}

-
-
- Unique Sessions: -

{ipSummary.uniqueSessions}

-
-
- Avg Time: -

{Math.round(ipSummary.averageTimeOnPage)}s

-
-
- Last Visit: -

{new Date(ipSummary.lastVisit).toLocaleDateString()}

-
-
- - {selectedIP === ipSummary.ip && ( -
- {/* Pages Visited */} -
-

Pages Visited:

-
- {Object.entries(ipSummary.pages).map(([page, count]) => ( - - {page}: {count} - - ))} -
-
- - {/* Devices */} -
-

Devices:

-
- {Object.entries(ipSummary.devices).map(([device, count]) => ( - - {device}: {count} - - ))} -
-
- - {/* Browsers */} -
-

Browsers:

-
- {Object.entries(ipSummary.browsers).map(([browser, count]) => ( - - {browser}: {count} - - ))} -
-
-
- )} -
- ))} -
- )} -
- - {/* Recent Events */} -
-

Recent Events

- {analyticsData.events.length === 0 ? ( -

No recent events found.

- ) : ( -
- {analyticsData.events.slice(0, 10).map((event, index) => ( -
-
-
- {event.eventType} - on {event.pageUrl} -
- - {new Date(event.timestamp).toLocaleString()} - -
-
- IP: {event.ipAddress} | {event.deviceInfo.browser} on {event.deviceInfo.deviceType} - {event.timeOnPage && ` | ${Math.round(event.timeOnPage)}s on page`} -
-
- ))} -
- )} -
-
- ) : ( -
-

Loading analytics data...

-
- )} -
+
+
) } \ No newline at end of file diff --git a/src/app/api/analytics/marketing-intelligence/route.ts b/src/app/api/analytics/marketing-intelligence/route.ts new file mode 100644 index 0000000..12d806c --- /dev/null +++ b/src/app/api/analytics/marketing-intelligence/route.ts @@ -0,0 +1,130 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { readdir, readFile } from 'fs/promises'; +import { join } from 'path'; +import { existsSync } from 'fs'; +import marketingIntelligenceService from '@/lib/marketingIntelligence'; + +interface TrackingEvent { + timestamp: string; + sessionId: string; + eventType: string; + pageUrl: string; + ipAddress: string; + userAgent: string; + deviceInfo: { + browser: string; + deviceType: string; + screenWidth: number; + screenHeight: number; + language: string; + }; + timeOnPage?: number; + scrollDepth?: number; + engagement?: { + mouseMovements: number; + clicks: number; + scrollEvents: number; + }; +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + let date = body.date; + + // If no date provided, use today's date + if (!date) { + const today = new Date(); + date = today.toISOString().split('T')[0]; // Format: YYYY-MM-DD + } + + const [year, month, day] = date.split('-'); + const logDir = join(process.env.HOME || '/home/bryanwi09', 'docker/nginx/logs/bigbraincoding.com', year, month, day); + + if (!existsSync(logDir)) { + return NextResponse.json({ + visitorProfiles: [], + salesIntelligence: { + highValueVisitors: [], + conversionOpportunities: [], + marketInsights: { + topPerformingPages: [], + commonUserJourneys: [], + devicePreferences: {}, + timeBasedTrends: {} + } + } + }); + } + + const files = await readdir(logDir); + const jsonFiles = files.filter(file => file.endsWith('.json') && !file.includes('summary')); + + const events: TrackingEvent[] = []; + const sessionData: Map; + }> = new Map(); + + // Read and parse all tracking events + for (const file of jsonFiles) { + try { + const content = await readFile(join(logDir, file), 'utf-8'); + const event: TrackingEvent = JSON.parse(content); + events.push(event); + + // Group events by session + const sessionKey = event.sessionId; + if (!sessionData.has(sessionKey)) { + sessionData.set(sessionKey, { + ip: event.ipAddress, + sessionId: event.sessionId, + pages: [], + timeOnSite: 0, + engagementMetrics: { + mouseMovements: 0, + clicks: 0, + scrollEvents: 0 + } + }); + } + + const session = sessionData.get(sessionKey)!; + session.pages.push(event.pageUrl); + session.timeOnSite += event.timeOnPage || 0; + + // Aggregate engagement metrics + if (event.engagement) { + session.engagementMetrics.mouseMovements += event.engagement.mouseMovements || 0; + session.engagementMetrics.clicks += event.engagement.clicks || 0; + session.engagementMetrics.scrollEvents += event.engagement.scrollEvents || 0; + } + } catch (error) { + console.error(`Error reading file ${file}:`, error); + } + } + + // Update visitor profiles with session data + for (const session of sessionData.values()) { + marketingIntelligenceService.updateVisitorProfile(session); + } + + // Get visitor profiles and sales intelligence + const visitorProfiles = marketingIntelligenceService.getVisitorProfiles(); + const salesIntelligence = marketingIntelligenceService.generateSalesIntelligence(); + + return NextResponse.json({ + visitorProfiles, + salesIntelligence + }); + } catch (error) { + console.error('Marketing Intelligence API error:', error); + return NextResponse.json( + { error: 'Failed to generate marketing intelligence' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/analytics/nginx-logs/route.ts b/src/app/api/analytics/nginx-logs/route.ts new file mode 100644 index 0000000..f06a4fe --- /dev/null +++ b/src/app/api/analytics/nginx-logs/route.ts @@ -0,0 +1,109 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { readdir } from 'fs/promises'; +import { join } from 'path'; +import { existsSync } from 'fs'; +import nginxLogParser from '@/lib/nginxLogParser'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { dateRange, logType = 'all' } = body; + + // Define log file paths + const logDir = join(process.env.HOME || '/home/bryanwi09', 'docker/nginx/logs'); + const logFiles = [ + join(logDir, 'bigbraincoding.com_access.log'), + join(logDir, 'bigbraincoding.com_tracking.log'), + join(logDir, 'bigbraincoding.com_ip_tracking.log') + ]; + + // Filter log files based on type + let selectedLogFiles: string[] = []; + switch (logType) { + case 'access': + selectedLogFiles = [logFiles[0]]; + break; + case 'tracking': + selectedLogFiles = [logFiles[1]]; + break; + case 'ip_tracking': + selectedLogFiles = [logFiles[2]]; + break; + default: + selectedLogFiles = logFiles.filter(file => existsSync(file)); + } + + if (selectedLogFiles.length === 0) { + return NextResponse.json({ + entries: [], + summary: { + totalRequests: 0, + uniqueIPs: [], + statusCodes: {}, + topPaths: {}, + topUserAgents: {}, + topReferers: {}, + averageResponseTime: 0, + totalBytesSent: 0, + timeRange: { start: '', end: '' } + } + }); + } + + // Parse log files + const entries = await nginxLogParser.parseLogFiles(selectedLogFiles); + + // Filter by date range if provided + let filteredEntries = entries; + if (dateRange && dateRange.start && dateRange.end) { + filteredEntries = nginxLogParser.filterByDateRange(entries, dateRange.start, dateRange.end); + } + + // Generate summary + const summary = nginxLogParser.generateSummary(filteredEntries); + + // Convert Set to Array for JSON serialization + const serializableSummary = { + ...summary, + uniqueIPs: Array.from(summary.uniqueIPs) + }; + + return NextResponse.json({ + entries: filteredEntries, + summary: serializableSummary, + logType, + filesProcessed: selectedLogFiles + }); + } catch (error) { + console.error('NGINX Logs API error:', error); + return NextResponse.json( + { error: 'Failed to parse NGINX logs' }, + { status: 500 } + ); + } +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const logType = searchParams.get('type') || 'all'; + const startDate = searchParams.get('start'); + const endDate = searchParams.get('end'); + + const body: any = { logType }; + if (startDate && endDate) { + body.dateRange = { start: startDate, end: endDate }; + } + + return POST(new NextRequest(request.url, { + method: 'POST', + body: JSON.stringify(body) + })); + } catch (error) { + console.error('NGINX Logs API GET error:', error); + return NextResponse.json( + { error: 'Failed to parse NGINX logs' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/tracking/route.ts b/src/app/api/tracking/route.ts index 14a2524..704e76c 100755 --- a/src/app/api/tracking/route.ts +++ b/src/app/api/tracking/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { writeFile, mkdir, readFile } from 'fs/promises'; import { join } from 'path'; import { existsSync } from 'fs'; +import botDetectionService from '@/lib/botDetection'; export async function POST(request: NextRequest) { try { @@ -19,6 +20,49 @@ export async function POST(request: NextRequest) { // Add IP address to event event.ipAddress = ip; + // Bot detection and rate limiting + const botDetectionResult = botDetectionService.detectBot( + ip, + event.userAgent || request.headers.get('user-agent') || '', + { + timeOnPage: event.timeOnPage, + mouseMovements: event.engagement?.mouseMovements || 0 + } + ); + + // Add bot detection data to event + event.botDetection = { + isBot: botDetectionResult.isBot, + confidence: botDetectionResult.confidence, + reason: botDetectionResult.reason, + requiresVerification: botDetectionResult.requiresVerification, + rateLimitExceeded: botDetectionResult.rateLimitExceeded + }; + + // If bot detected with high confidence, log but don't process normally + if (botDetectionResult.isBot && botDetectionResult.confidence > 0.8) { + console.log('Bot detected:', { + ip, + userAgent: event.userAgent, + confidence: botDetectionResult.confidence, + reason: botDetectionResult.reason + }); + + // Still log the event but mark it as bot activity + event.eventType = `bot_${event.eventType}`; + } + + // If rate limited, return error + if (botDetectionResult.rateLimitExceeded) { + return NextResponse.json( + { + error: 'Rate limit exceeded', + resetTime: Date.now() + 60 * 60 * 1000 // 1 hour from now + }, + { status: 429 } + ); + } + // Check if we're on Vercel const isVercel = process.env.VERCEL === '1'; @@ -35,8 +79,12 @@ export async function POST(request: NextRequest) { deviceInfo: event.deviceInfo, timeOnPage: event.timeOnPage, scrollDepth: event.scrollDepth, + botDetection: event.botDetection + }); + return NextResponse.json({ + message: 'Event received (Vercel environment)', + requiresVerification: botDetectionResult.requiresVerification }); - return NextResponse.json({ message: 'Event received (Vercel environment)' }); } // For local/self-hosted environment, write to file @@ -71,6 +119,7 @@ export async function POST(request: NextRequest) { uniqueSessions: new Set(), pageViews: 0, clicks: 0, + botEvents: 0, ipAddresses: {} as { [key: string]: { totalVisits: number, uniqueSessions: Set } } }; @@ -80,6 +129,7 @@ export async function POST(request: NextRequest) { dailySummary.totalEvents = parsedSummary.totalEvents || 0; dailySummary.pageViews = parsedSummary.pageViews || 0; dailySummary.clicks = parsedSummary.clicks || 0; + dailySummary.botEvents = parsedSummary.botEvents || 0; dailySummary.uniqueVisitors = new Set(parsedSummary.uniqueVisitors || []); dailySummary.uniqueSessions = new Set(parsedSummary.uniqueSessions || []); for (const ip in parsedSummary.ipAddresses) { @@ -100,6 +150,11 @@ export async function POST(request: NextRequest) { dailySummary.clicks++; } + // Track bot events separately + if (botDetectionResult.isBot) { + dailySummary.botEvents++; + } + if (!dailySummary.ipAddresses[event.ipAddress]) { dailySummary.ipAddresses[event.ipAddress] = { totalVisits: 0, uniqueSessions: new Set() }; } @@ -120,7 +175,11 @@ export async function POST(request: NextRequest) { await writeFile(summaryFilePath, JSON.stringify(serializableSummary, null, 2)); - return NextResponse.json({ message: 'Event received and logged' }); + return NextResponse.json({ + message: 'Event received and logged', + requiresVerification: botDetectionResult.requiresVerification, + botDetected: botDetectionResult.isBot + }); } catch (error) { console.error('Tracking API error:', error); return NextResponse.json( diff --git a/src/components/analytics/EnhancedAnalyticsDashboard.tsx b/src/components/analytics/EnhancedAnalyticsDashboard.tsx new file mode 100644 index 0000000..ac95124 --- /dev/null +++ b/src/components/analytics/EnhancedAnalyticsDashboard.tsx @@ -0,0 +1,407 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Progress } from '@/components/ui/progress' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { + Activity, + Globe, + Server, + BarChart3, + Users, + Clock, + Download, + Eye, + MousePointer, + TrendingUp, + AlertTriangle +} from 'lucide-react' + +interface NGINXLogEntry { + timestamp: string; + ipAddress: string; + method: string; + path: string; + statusCode: number; + bytesSent: number; + referer: string; + userAgent: string; + requestTime: number; + upstreamResponseTime: number; +} + +interface NGINXLogSummary { + totalRequests: number; + uniqueIPs: string[]; + statusCodes: Record; + topPaths: Record; + topUserAgents: Record; + topReferers: Record; + averageResponseTime: number; + totalBytesSent: number; + timeRange: { + start: string; + end: string; + }; +} + +interface AnalyticsData { + nginx: { + entries: NGINXLogEntry[]; + summary: NGINXLogSummary; + }; + tracking: { + entries: any[]; + summary: any; + }; + marketing: { + visitorProfiles: any[]; + salesIntelligence: any; + }; +} + +export default function EnhancedAnalyticsDashboard() { + const [analyticsData, setAnalyticsData] = useState(null) + const [loading, setLoading] = useState(true) + const [selectedPlatform, setSelectedPlatform] = useState<'nginx' | 'tracking' | 'marketing'>('nginx') + const [selectedLogType, setSelectedLogType] = useState<'all' | 'access' | 'tracking' | 'ip_tracking'>('all') + const [dateRange, setDateRange] = useState({ + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], + end: new Date().toISOString().split('T')[0] + }) + + useEffect(() => { + fetchAnalyticsData() + }, [selectedLogType, dateRange]) + + const fetchAnalyticsData = async () => { + try { + setLoading(true) + + // Fetch NGINX logs + const nginxResponse = await fetch('/api/analytics/nginx-logs', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + logType: selectedLogType, + dateRange + }) + }) + + // Fetch tracking data + const trackingResponse = await fetch('/api/analytics/dashboard', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ dateRange }) + }) + + // Fetch marketing intelligence + const marketingResponse = await fetch('/api/analytics/marketing-intelligence', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ dateRange }) + }) + + const nginxData = nginxResponse.ok ? await nginxResponse.json() : { entries: [], summary: {} } + const trackingData = trackingResponse.ok ? await trackingResponse.json() : { entries: [], summary: {} } + const marketingData = marketingResponse.ok ? await marketingResponse.json() : { visitorProfiles: [], salesIntelligence: {} } + + setAnalyticsData({ + nginx: nginxData, + tracking: trackingData, + marketing: marketingData + }) + } catch (error) { + console.error('Error fetching analytics data:', error) + } finally { + setLoading(false) + } + } + + const formatBytes = (bytes: number) => { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } + + const formatDuration = (ms: number) => { + const seconds = Math.floor(ms / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + + if (hours > 0) return `${hours}h ${minutes % 60}m` + if (minutes > 0) return `${minutes}m ${seconds % 60}s` + return `${seconds}s` + } + + const getStatusColor = (statusCode: number) => { + if (statusCode >= 200 && statusCode < 300) return 'text-green-600' + if (statusCode >= 300 && statusCode < 400) return 'text-yellow-600' + if (statusCode >= 400 && statusCode < 500) return 'text-orange-600' + if (statusCode >= 500) return 'text-red-600' + return 'text-gray-600' + } + + if (loading) { + return ( +
+
Loading analytics data...
+
+ ) + } + + return ( +
+
+
+

Analytics Dashboard

+

+ Comprehensive analytics from multiple data sources +

+
+
+ + +
+
+ + setSelectedPlatform(value)} className="space-y-4"> + + + + NGINX Logs + + + + Tracking Data + + + + Marketing Intelligence + + + + + {analyticsData?.nginx && ( + <> +
+ + + Total Requests + + + +
{analyticsData.nginx.summary.totalRequests}
+

+ {selectedLogType === 'all' ? 'All log types' : `${selectedLogType} logs`} +

+
+
+ + + + Unique Visitors + + + +
{analyticsData.nginx.summary.uniqueIPs.length}
+

+ Different IP addresses +

+
+
+ + + + Avg Response Time + + + +
+ {formatDuration(analyticsData.nginx.summary.averageResponseTime * 1000)} +
+

+ Average request time +

+
+
+ + + + Data Transferred + + + +
+ {formatBytes(analyticsData.nginx.summary.totalBytesSent)} +
+

+ Total bytes sent +

+
+
+
+ +
+ + + Top Pages + + Most requested pages + + + +
+ {Object.entries(analyticsData.nginx.summary.topPaths) + .sort(([, a], [, b]) => b - a) + .slice(0, 10) + .map(([path, count]) => ( +
+
+
+ {path} +
+ {count} +
+ ))} +
+
+
+ + + + Status Codes + + HTTP response status distribution + + + +
+ {Object.entries(analyticsData.nginx.summary.statusCodes) + .sort(([, a], [, b]) => b - a) + .map(([status, count]) => ( +
+
+
+ {status} +
+ {count} +
+ ))} +
+
+
+
+ + + + Recent Requests + + Latest NGINX log entries + + + +
+ {analyticsData.nginx.entries + .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) + .slice(0, 20) + .map((entry, index) => ( +
+
+
+
+
+
{entry.ipAddress}
+
+ {entry.method} {entry.path} +
+
+
+
+
+ {entry.statusCode} +
+
+ {new Date(entry.timestamp).toLocaleString()} +
+
+
+
+ ))} +
+
+
+ + )} +
+ + + {analyticsData?.tracking && ( + + + Tracking Data + + Custom tracking events and analytics + + + +
+ +

+ Tracking data will be displayed here when available +

+
+
+
+ )} +
+ + + {analyticsData?.marketing && ( + + + Marketing Intelligence + + Lead qualification and sales intelligence + + + +
+ +

+ Marketing intelligence data will be displayed here when available +

+
+
+
+ )} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/analytics/MarketingIntelligenceDashboard.tsx b/src/components/analytics/MarketingIntelligenceDashboard.tsx new file mode 100644 index 0000000..44e4406 --- /dev/null +++ b/src/components/analytics/MarketingIntelligenceDashboard.tsx @@ -0,0 +1,538 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Progress } from '@/components/ui/progress' +import { + Users, + TrendingUp, + Target, + Clock, + MapPin, + Activity, + AlertCircle, + CheckCircle, + XCircle +} from 'lucide-react' + +interface VisitorProfile { + ip: string + sessionId: string + firstVisit: string + lastVisit: string + totalVisits: number + totalTimeOnSite: number + pagesVisited: string[] + engagementScore: number + leadScore: number + deviceConsistency: boolean + highEngagementPages: string[] + conversionEvents: string[] + timeBasedPatterns: { + averageSessionDuration: number + preferredVisitTimes: string[] + returnVisitor: boolean + } +} + +interface LeadQualification { + isQualified: boolean + leadScore: number + qualificationReason: string + recommendedAction: string + urgency: 'low' | 'medium' | 'high' + nextBestAction: string +} + +interface SalesIntelligence { + highValueVisitors: VisitorProfile[] + conversionOpportunities: { + visitor: VisitorProfile + opportunity: string + confidence: number + }[] + marketInsights: { + topPerformingPages: string[] + commonUserJourneys: string[][] + devicePreferences: Record + timeBasedTrends: Record + } +} + +export default function MarketingIntelligenceDashboard() { + const [visitorProfiles, setVisitorProfiles] = useState([]) + const [salesIntelligence, setSalesIntelligence] = useState(null) + const [loading, setLoading] = useState(true) + const [selectedVisitor, setSelectedVisitor] = useState(null) + + useEffect(() => { + fetchMarketingIntelligence() + }, []) + + const fetchMarketingIntelligence = async () => { + try { + setLoading(true) + + // This would be replaced with actual API calls + const response = await fetch('/api/analytics/marketing-intelligence', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ dateRange: 'today' }) + }) + + if (response.ok) { + const data = await response.json() + setVisitorProfiles(data.visitorProfiles || []) + setSalesIntelligence(data.salesIntelligence || null) + } else { + // Mock data for demonstration + setVisitorProfiles([ + { + ip: '192.168.1.100', + sessionId: 'session-1', + firstVisit: '2025-07-31T10:00:00.000Z', + lastVisit: '2025-07-31T15:30:00.000Z', + totalVisits: 3, + totalTimeOnSite: 1800000, // 30 minutes + pagesVisited: ['/', '/services', '/projects', '/contact'], + engagementScore: 0.85, + leadScore: 0.78, + deviceConsistency: true, + highEngagementPages: ['/services', '/projects', '/contact'], + conversionEvents: ['contact_page_visited', 'services_page_visited'], + timeBasedPatterns: { + averageSessionDuration: 600000, // 10 minutes + preferredVisitTimes: ['10:00', '14:00', '15:00'], + returnVisitor: true + } + }, + { + ip: '192.168.1.101', + sessionId: 'session-2', + firstVisit: '2025-07-31T12:00:00.000Z', + lastVisit: '2025-07-31T12:15:00.000Z', + totalVisits: 1, + totalTimeOnSite: 900000, // 15 minutes + pagesVisited: ['/', '/about'], + engagementScore: 0.45, + leadScore: 0.32, + deviceConsistency: false, + highEngagementPages: ['/about'], + conversionEvents: [], + timeBasedPatterns: { + averageSessionDuration: 900000, + preferredVisitTimes: ['12:00'], + returnVisitor: false + } + } + ]) + + setSalesIntelligence({ + highValueVisitors: [ + { + ip: '192.168.1.100', + sessionId: 'session-1', + firstVisit: '2025-07-31T10:00:00.000Z', + lastVisit: '2025-07-31T15:30:00.000Z', + totalVisits: 3, + totalTimeOnSite: 1800000, + pagesVisited: ['/', '/services', '/projects', '/contact'], + engagementScore: 0.85, + leadScore: 0.78, + deviceConsistency: true, + highEngagementPages: ['/services', '/projects', '/contact'], + conversionEvents: ['contact_page_visited', 'services_page_visited'], + timeBasedPatterns: { + averageSessionDuration: 600000, + preferredVisitTimes: ['10:00', '14:00', '15:00'], + returnVisitor: true + } + } + ], + conversionOpportunities: [ + { + visitor: { + ip: '192.168.1.101', + sessionId: 'session-2', + firstVisit: '2025-07-31T12:00:00.000Z', + lastVisit: '2025-07-31T12:15:00.000Z', + totalVisits: 1, + totalTimeOnSite: 900000, + pagesVisited: ['/', '/about'], + engagementScore: 0.45, + leadScore: 0.32, + deviceConsistency: false, + highEngagementPages: ['/about'], + conversionEvents: [], + timeBasedPatterns: { + averageSessionDuration: 900000, + preferredVisitTimes: ['12:00'], + returnVisitor: false + } + }, + opportunity: 'Contact page conversion', + confidence: 0.32 + } + ], + marketInsights: { + topPerformingPages: ['/', '/services', '/projects', '/contact', '/about'], + commonUserJourneys: [ + ['/', '/services'], + ['/', '/projects'], + ['/', '/contact'] + ], + devicePreferences: { desktop: 70, mobile: 25, tablet: 5 }, + timeBasedTrends: { '10:00': 3, '12:00': 1, '14:00': 2, '15:00': 1 } + } + }) + } + } catch (error) { + console.error('Error fetching marketing intelligence:', error) + } finally { + setLoading(false) + } + } + + const getUrgencyColor = (urgency: string) => { + switch (urgency) { + case 'high': return 'bg-red-500' + case 'medium': return 'bg-yellow-500' + case 'low': return 'bg-green-500' + default: return 'bg-gray-500' + } + } + + const getLeadScoreColor = (score: number) => { + if (score >= 0.7) return 'text-green-600' + if (score >= 0.4) return 'text-yellow-600' + return 'text-red-600' + } + + const formatDuration = (ms: number) => { + const minutes = Math.floor(ms / 60000) + const seconds = Math.floor((ms % 60000) / 1000) + return `${minutes}m ${seconds}s` + } + + if (loading) { + return ( +
+
Loading marketing intelligence...
+
+ ) + } + + return ( +
+
+
+

Marketing Intelligence

+

+ Lead qualification and sales intelligence dashboard +

+
+ +
+ + + + Lead Qualification + Sales Intelligence + Market Insights + + + +
+ + + Total Visitors + + + +
{visitorProfiles.length}
+

+ Active in last 24 hours +

+
+
+ + + + Qualified Leads + + + +
+ {visitorProfiles.filter(v => v.leadScore >= 0.6).length} +
+

+ High engagement visitors +

+
+
+ + + + Avg Engagement + + + +
+ {Math.round(visitorProfiles.reduce((acc, v) => acc + v.engagementScore, 0) / visitorProfiles.length * 100)}% +
+

+ Across all visitors +

+
+
+
+ + + + Visitor Profiles + + Detailed visitor analysis and lead qualification + + + +
+ {visitorProfiles.map((visitor) => ( +
setSelectedVisitor(visitor)} + > +
+
+
+
+
{visitor.ip}
+
+ {visitor.totalVisits} visits • {formatDuration(visitor.totalTimeOnSite)} +
+
+
+
+
+ {Math.round(visitor.leadScore * 100)}% +
+
Lead Score
+
+
+ +
+
+ + {visitor.timeBasedPatterns.returnVisitor ? "Return Visitor" : "New Visitor"} + + + {visitor.highEngagementPages.length} key pages + + + {visitor.conversionEvents.length} conversions + +
+ +
+ + Last visit: {new Date(visitor.lastVisit).toLocaleString()} +
+
+
+ ))} +
+
+
+
+ + + {salesIntelligence && ( + <> +
+ + + High Value Visitors + + Visitors with lead score ≥ 60% + + + +
+ {salesIntelligence.highValueVisitors.map((visitor) => ( +
+
+
{visitor.ip}
+
+ {visitor.conversionEvents.join(', ')} +
+
+
+
+ {Math.round(visitor.leadScore * 100)}% +
+
Score
+
+
+ ))} +
+
+
+ + + + Conversion Opportunities + + Visitors ready for follow-up + + + +
+ {salesIntelligence.conversionOpportunities.map((opp) => ( +
+
+
{opp.visitor.ip}
+ + {Math.round(opp.confidence * 100)}% confidence + +
+
+ {opp.opportunity} +
+
+ ))} +
+
+
+
+ + )} +
+ + + {salesIntelligence && ( + <> +
+ + + Top Performing Pages + + Most engaging content + + + +
+ {salesIntelligence.marketInsights.topPerformingPages.map((page, index) => ( +
+
+
+ {index + 1} +
+ {page} +
+
+ ))} +
+
+
+ + + + Common User Journeys + + Most frequent navigation paths + + + +
+ {salesIntelligence.marketInsights.commonUserJourneys.map((journey, index) => ( +
+
+ Journey {index + 1} +
+
+ {journey.join(' → ')} +
+
+ ))} +
+
+
+
+ + )} +
+
+ + {/* Visitor Detail Modal */} + {selectedVisitor && ( +
+
+
+

Visitor Details

+ +
+ +
+
+
+
IP Address
+
{selectedVisitor.ip}
+
+
+
Lead Score
+
+ {Math.round(selectedVisitor.leadScore * 100)}% +
+
+
+
Total Visits
+
{selectedVisitor.totalVisits}
+
+
+
Total Time
+
{formatDuration(selectedVisitor.totalTimeOnSite)}
+
+
+ +
+
Pages Visited
+
+ {selectedVisitor.pagesVisited.map((page) => ( + {page} + ))} +
+
+ +
+
Conversion Events
+
+ {selectedVisitor.conversionEvents.map((event) => ( + {event} + ))} +
+
+ +
+
Engagement Score
+ +
+ {Math.round(selectedVisitor.engagementScore * 100)}% engagement +
+
+
+
+
+ )} +
+ ) +} \ No newline at end of file diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 0000000..e391a79 --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +function Progress({ + className, + value, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Progress } \ No newline at end of file diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..c546248 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,160 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} \ No newline at end of file diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..5959555 --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent } \ No newline at end of file diff --git a/src/lib/botDetection.ts b/src/lib/botDetection.ts new file mode 100644 index 0000000..0f23a5f --- /dev/null +++ b/src/lib/botDetection.ts @@ -0,0 +1,208 @@ +// Bot Detection and Rate Limiting System +export interface BotDetectionConfig { + maxRequestsPerMinute: number; + maxRequestsPerHour: number; + suspiciousUserAgents: string[]; + progressiveVerificationEnabled: boolean; +} + +export interface BotDetectionResult { + isBot: boolean; + confidence: number; + reason: string; + requiresVerification: boolean; + rateLimitExceeded: boolean; +} + +export interface RateLimitInfo { + requestsInLastMinute: number; + requestsInLastHour: number; + isRateLimited: boolean; + resetTime: number; +} + +class BotDetectionService { + private requestCounts: Map = new Map(); + private config: BotDetectionConfig; + + constructor(config: Partial = {}) { + this.config = { + maxRequestsPerMinute: 60, + maxRequestsPerHour: 1000, + suspiciousUserAgents: [ + 'bot', 'crawler', 'spider', 'scraper', 'curl', 'wget', 'python', 'java', + 'go-http-client', 'okhttp', 'apache-httpclient', 'postman', 'insomnia' + ], + progressiveVerificationEnabled: true, + ...config + }; + } + + /** + * Analyze user agent for bot indicators + */ + private analyzeUserAgent(userAgent: string): { isBot: boolean; confidence: number; reason: string } { + const ua = userAgent.toLowerCase(); + + // Check for known bot patterns + for (const pattern of this.config.suspiciousUserAgents) { + if (ua.includes(pattern)) { + return { + isBot: true, + confidence: 0.9, + reason: `Suspicious user agent pattern: ${pattern}` + }; + } + } + + // Check for missing common browser indicators + const hasBrowserIndicators = ua.includes('mozilla') || ua.includes('chrome') || ua.includes('safari') || ua.includes('firefox'); + if (!hasBrowserIndicators && ua.length < 50) { + return { + isBot: true, + confidence: 0.7, + reason: 'Missing browser indicators and short user agent' + }; + } + + // Check for automation tools + const automationTools = ['selenium', 'webdriver', 'phantomjs', 'headless']; + for (const tool of automationTools) { + if (ua.includes(tool)) { + return { + isBot: true, + confidence: 0.95, + reason: `Automation tool detected: ${tool}` + }; + } + } + + return { + isBot: false, + confidence: 0.1, + reason: 'Appears to be legitimate browser' + }; + } + + /** + * Check rate limiting for IP address + */ + private checkRateLimit(ip: string): RateLimitInfo { + const now = Date.now(); + const minuteAgo = now - 60 * 1000; + const hourAgo = now - 60 * 60 * 1000; + + const ipData = this.requestCounts.get(ip) || { minute: 0, hour: 0, lastReset: now }; + + // Reset counters if needed + if (now - ipData.lastReset > 60 * 60 * 1000) { + ipData.minute = 0; + ipData.hour = 0; + ipData.lastReset = now; + } + + // Increment counters + ipData.minute++; + ipData.hour++; + + this.requestCounts.set(ip, ipData); + + const isRateLimited = + ipData.minute > this.config.maxRequestsPerMinute || + ipData.hour > this.config.maxRequestsPerHour; + + return { + requestsInLastMinute: ipData.minute, + requestsInLastHour: ipData.hour, + isRateLimited, + resetTime: ipData.lastReset + 60 * 60 * 1000 + }; + } + + /** + * Main bot detection method + */ + public detectBot(ip: string, userAgent: string, additionalData?: Record): BotDetectionResult { + // Check rate limiting first + const rateLimitInfo = this.checkRateLimit(ip); + + // Analyze user agent + const uaAnalysis = this.analyzeUserAgent(userAgent); + + // Additional heuristics + let confidence = uaAnalysis.confidence; + let reason = uaAnalysis.reason; + let requiresVerification = false; + + // Check for suspicious behavior patterns + if (additionalData) { + // Check for rapid page changes + if (additionalData.timeOnPage && (additionalData.timeOnPage as number) < 1000) { + confidence = Math.min(confidence + 0.2, 1.0); + reason += '; Rapid page navigation detected'; + } + + // Check for missing mouse movements (if available) + if (additionalData.mouseMovements === 0) { + confidence = Math.min(confidence + 0.1, 1.0); + reason += '; No mouse movements detected'; + } + } + + // Determine if verification is required + if (this.config.progressiveVerificationEnabled && confidence > 0.5) { + requiresVerification = true; + } + + return { + isBot: uaAnalysis.isBot || rateLimitInfo.isRateLimited, + confidence, + reason, + requiresVerification, + rateLimitExceeded: rateLimitInfo.isRateLimited + }; + } + + /** + * Generate verification challenge for suspicious activity + */ + public generateVerificationChallenge(): { type: string; challenge: string; expiresAt: number } { + const challenges = [ + { type: 'press_and_hold', challenge: 'Press and hold this button for 3 seconds' }, + { type: 'simple_math', challenge: 'What is 7 + 3?' }, + { type: 'checkbox', challenge: 'Check this box to continue' } + ]; + + const randomChallenge = challenges[Math.floor(Math.random() * challenges.length)]; + + return { + type: randomChallenge.type, + challenge: randomChallenge.challenge, + expiresAt: Date.now() + 5 * 60 * 1000 // 5 minutes + }; + } + + /** + * Clean up old rate limit data + */ + public cleanup(): void { + const now = Date.now(); + const oneHourAgo = now - 60 * 60 * 1000; + + for (const [ip, data] of this.requestCounts.entries()) { + if (data.lastReset < oneHourAgo) { + this.requestCounts.delete(ip); + } + } + } +} + +// Export singleton instance +export const botDetectionService = new BotDetectionService(); + +// Cleanup old data every hour +setInterval(() => { + botDetectionService.cleanup(); +}, 60 * 60 * 1000); + +export default botDetectionService; \ No newline at end of file diff --git a/src/lib/marketingIntelligence.ts b/src/lib/marketingIntelligence.ts new file mode 100644 index 0000000..696b923 --- /dev/null +++ b/src/lib/marketingIntelligence.ts @@ -0,0 +1,489 @@ +// Marketing Intelligence and Lead Qualification System +export interface VisitorProfile { + ip: string; + sessionId: string; + firstVisit: string; + lastVisit: string; + totalVisits: number; + totalTimeOnSite: number; + pagesVisited: string[]; + engagementScore: number; + leadScore: number; + deviceConsistency: boolean; + highEngagementPages: string[]; + conversionEvents: string[]; + timeBasedPatterns: { + averageSessionDuration: number; + preferredVisitTimes: string[]; + returnVisitor: boolean; + }; +} + +export interface LeadQualification { + isQualified: boolean; + leadScore: number; + qualificationReason: string; + recommendedAction: string; + urgency: 'low' | 'medium' | 'high'; + nextBestAction: string; +} + +export interface SalesIntelligence { + highValueVisitors: VisitorProfile[]; + conversionOpportunities: { + visitor: VisitorProfile; + opportunity: string; + confidence: number; + }[]; + marketInsights: { + topPerformingPages: string[]; + commonUserJourneys: string[][]; + devicePreferences: Record; + timeBasedTrends: Record; + }; +} + +class MarketingIntelligenceService { + private visitorProfiles: Map = new Map(); + private highEngagementThresholds = { + timeOnSite: 300000, // 5 minutes + pagesVisited: 3, + engagementScore: 0.7, + leadScore: 0.6 + }; + + /** + * Update visitor profile with new session data + */ + public updateVisitorProfile(sessionData: { + ip: string; + sessionId: string; + pages: string[]; + timeOnSite: number; + engagementMetrics?: Record; + }): void { + const { ip, sessionId, pages, timeOnSite, engagementMetrics } = sessionData; + + const existingProfile = this.visitorProfiles.get(ip); + const now = new Date().toISOString(); + + const engagementScore = this.calculateEngagementScore({ + timeOnSite, + pagesVisited: pages.length, + engagementMetrics + }); + + const leadScore = this.calculateLeadScore({ + engagementScore, + totalVisits: (existingProfile?.totalVisits || 0) + 1, + timeOnSite, + pagesVisited: pages.length, + highValuePages: this.getHighValuePages(pages) + }); + + const profile: VisitorProfile = { + ip, + sessionId, + firstVisit: existingProfile?.firstVisit || now, + lastVisit: now, + totalVisits: (existingProfile?.totalVisits || 0) + 1, + totalTimeOnSite: (existingProfile?.totalTimeOnSite || 0) + timeOnSite, + pagesVisited: [...new Set([...(existingProfile?.pagesVisited || []), ...pages])], + engagementScore, + leadScore, + deviceConsistency: this.checkDeviceConsistency(existingProfile), + highEngagementPages: this.identifyHighEngagementPages(pages), + conversionEvents: this.trackConversionEvents(pages), + timeBasedPatterns: this.analyzeTimePatterns(existingProfile, now, timeOnSite) + }; + + this.visitorProfiles.set(ip, profile); + } + + /** + * Calculate engagement score based on visitor behavior + */ + private calculateEngagementScore(data: { + timeOnSite: number; + pagesVisited: number; + engagementMetrics?: Record; + }): number { + const { timeOnSite, pagesVisited, engagementMetrics } = data; + + let score = 0; + + // Time on site factor (0-40 points) + const timeScore = Math.min(timeOnSite / this.highEngagementThresholds.timeOnSite, 1) * 40; + score += timeScore; + + // Pages visited factor (0-30 points) + const pagesScore = Math.min(pagesVisited / this.highEngagementThresholds.pagesVisited, 1) * 30; + score += pagesScore; + + // Engagement metrics factor (0-30 points) + if (engagementMetrics) { + const scrollDepth = engagementMetrics.scrollDepth || 0; + const clicks = engagementMetrics.clicks || 0; + const mouseMovements = engagementMetrics.mouseMovements || 0; + + const engagementScore = ( + (scrollDepth / 100) * 10 + + Math.min(clicks / 10, 1) * 10 + + Math.min(mouseMovements / 50, 1) * 10 + ); + score += engagementScore; + } + + return Math.min(score / 100, 1); + } + + /** + * Calculate lead score for qualification + */ + private calculateLeadScore(data: { + engagementScore: number; + totalVisits: number; + timeOnSite: number; + pagesVisited: number; + highValuePages: string[]; + }): number { + const { engagementScore, totalVisits, timeOnSite, pagesVisited, highValuePages } = data; + + let score = 0; + + // Engagement factor (0-30 points) + score += engagementScore * 30; + + // Return visitor factor (0-20 points) + if (totalVisits > 1) { + score += Math.min(totalVisits / 5, 1) * 20; + } + + // Time investment factor (0-25 points) + const timeScore = Math.min(timeOnSite / (this.highEngagementThresholds.timeOnSite * 2), 1) * 25; + score += timeScore; + + // High-value page factor (0-25 points) + const highValueScore = Math.min(highValuePages.length / 3, 1) * 25; + score += highValueScore; + + return Math.min(score / 100, 1); + } + + /** + * Identify high-value pages that indicate serious interest + */ + private getHighValuePages(pages: string[]): string[] { + const highValuePagePatterns = [ + '/services', + '/projects', + '/contact', + '/about', + '/pricing', + '/quote' + ]; + + return pages.filter(page => + highValuePagePatterns.some(pattern => page.includes(pattern)) + ); + } + + /** + * Check device consistency across visits + */ + private checkDeviceConsistency(existingProfile?: VisitorProfile): boolean { + // For now, assume consistent if profile exists + // In a real implementation, you'd compare device fingerprints + return !!existingProfile; + } + + /** + * Identify pages that indicate high engagement + */ + private identifyHighEngagementPages(pages: string[]): string[] { + const engagementIndicators = [ + 'contact', + 'services', + 'projects', + 'about', + 'pricing' + ]; + + return pages.filter(page => + engagementIndicators.some(indicator => page.includes(indicator)) + ); + } + + /** + * Track conversion events + */ + private trackConversionEvents(pages: string[]): string[] { + const conversionEvents: string[] = []; + + if (pages.includes('/contact')) { + conversionEvents.push('contact_page_visited'); + } + + if (pages.includes('/services')) { + conversionEvents.push('services_page_visited'); + } + + if (pages.includes('/projects')) { + conversionEvents.push('portfolio_viewed'); + } + + return conversionEvents; + } + + /** + * Analyze time-based patterns + */ + private analyzeTimePatterns( + existingProfile: VisitorProfile | undefined, + currentVisit: string, + sessionDuration: number + ): VisitorProfile['timeBasedPatterns'] { + const now = new Date(currentVisit); + const visitHour = now.getHours(); + + const patterns: VisitorProfile['timeBasedPatterns'] = { + averageSessionDuration: existingProfile?.timeBasedPatterns.averageSessionDuration || sessionDuration, + preferredVisitTimes: existingProfile?.timeBasedPatterns.preferredVisitTimes || [], + returnVisitor: !!existingProfile + }; + + // Update preferred visit times + const hourString = `${visitHour}:00`; + if (!patterns.preferredVisitTimes.includes(hourString)) { + patterns.preferredVisitTimes.push(hourString); + } + + // Update average session duration + if (existingProfile) { + const totalSessions = existingProfile.totalVisits; + patterns.averageSessionDuration = ( + (existingProfile.timeBasedPatterns.averageSessionDuration * (totalSessions - 1) + sessionDuration) / totalSessions + ); + } + + return patterns; + } + + /** + * Qualify a visitor as a lead + */ + public qualifyLead(ip: string): LeadQualification { + const profile = this.visitorProfiles.get(ip); + + if (!profile) { + return { + isQualified: false, + leadScore: 0, + qualificationReason: 'No visitor profile found', + recommendedAction: 'Continue monitoring', + urgency: 'low', + nextBestAction: 'Wait for more engagement' + }; + } + + const isQualified = profile.leadScore >= this.highEngagementThresholds.leadScore; + const urgency = this.determineUrgency(profile); + const recommendedAction = this.getRecommendedAction(profile); + const nextBestAction = this.getNextBestAction(profile); + + return { + isQualified, + leadScore: profile.leadScore, + qualificationReason: this.getQualificationReason(profile), + recommendedAction, + urgency, + nextBestAction + }; + } + + /** + * Determine urgency level for lead + */ + private determineUrgency(profile: VisitorProfile): 'low' | 'medium' | 'high' { + if (profile.leadScore >= 0.8) return 'high'; + if (profile.leadScore >= 0.6) return 'medium'; + return 'low'; + } + + /** + * Get recommended action for lead + */ + private getRecommendedAction(profile: VisitorProfile): string { + if (profile.conversionEvents.includes('contact_page_visited')) { + return 'Follow up on contact form submission'; + } + + if (profile.highEngagementPages.includes('/services')) { + return 'Send personalized service proposal'; + } + + if (profile.highEngagementPages.includes('/projects')) { + return 'Share relevant case studies'; + } + + return 'Send welcome email with value proposition'; + } + + /** + * Get next best action + */ + private getNextBestAction(profile: VisitorProfile): string { + if (!profile.conversionEvents.includes('contact_page_visited')) { + return 'Encourage contact page visit'; + } + + if (!profile.highEngagementPages.includes('/services')) { + return 'Direct to services page'; + } + + if (!profile.highEngagementPages.includes('/projects')) { + return 'Showcase portfolio'; + } + + return 'Maintain relationship with regular updates'; + } + + /** + * Get qualification reason + */ + private getQualificationReason(profile: VisitorProfile): string { + const reasons: string[] = []; + + if (profile.engagementScore > 0.7) { + reasons.push('High engagement'); + } + + if (profile.totalVisits > 2) { + reasons.push('Return visitor'); + } + + if (profile.highEngagementPages.length > 0) { + reasons.push('Viewed key pages'); + } + + if (profile.conversionEvents.length > 0) { + reasons.push('Conversion events triggered'); + } + + return reasons.join(', ') || 'Limited engagement'; + } + + /** + * Generate sales intelligence report + */ + public generateSalesIntelligence(): SalesIntelligence { + const profiles = Array.from(this.visitorProfiles.values()); + + const highValueVisitors = profiles.filter(p => p.leadScore >= 0.6); + + const conversionOpportunities = profiles + .filter(p => p.leadScore >= 0.4 && p.leadScore < 0.6) + .map(visitor => ({ + visitor, + opportunity: this.getConversionOpportunity(visitor), + confidence: visitor.leadScore + })); + + const marketInsights = this.generateMarketInsights(profiles); + + return { + highValueVisitors, + conversionOpportunities, + marketInsights + }; + } + + /** + * Get conversion opportunity for visitor + */ + private getConversionOpportunity(visitor: VisitorProfile): string { + if (!visitor.conversionEvents.includes('contact_page_visited')) { + return 'Contact page conversion'; + } + + if (!visitor.highEngagementPages.includes('/services')) { + return 'Service interest qualification'; + } + + if (!visitor.highEngagementPages.includes('/projects')) { + return 'Portfolio engagement'; + } + + return 'General engagement improvement'; + } + + /** + * Generate market insights from visitor data + */ + private generateMarketInsights(profiles: VisitorProfile[]): SalesIntelligence['marketInsights'] { + const pageVisits: Record = {}; + const devicePreferences: Record = {}; + const timeBasedTrends: Record = {}; + const userJourneys: string[][] = []; + + profiles.forEach(profile => { + // Count page visits + profile.pagesVisited.forEach(page => { + pageVisits[page] = (pageVisits[page] || 0) + 1; + }); + + // Track user journeys + userJourneys.push(profile.pagesVisited); + + // Time-based trends (simplified) + profile.timeBasedPatterns.preferredVisitTimes.forEach(time => { + timeBasedTrends[time] = (timeBasedTrends[time] || 0) + 1; + }); + }); + + return { + topPerformingPages: Object.entries(pageVisits) + .sort(([,a], [,b]) => b - a) + .slice(0, 5) + .map(([page]) => page), + commonUserJourneys: this.findCommonJourneys(userJourneys), + devicePreferences, + timeBasedTrends + }; + } + + /** + * Find common user journey patterns + */ + private findCommonJourneys(journeys: string[][]): string[][] { + const journeyCounts: Record = {}; + + journeys.forEach(journey => { + const key = journey.join(' -> '); + journeyCounts[key] = (journeyCounts[key] || 0) + 1; + }); + + return Object.entries(journeyCounts) + .sort(([,a], [,b]) => b - a) + .slice(0, 3) + .map(([journey]) => journey.split(' -> ')); + } + + /** + * Get all visitor profiles + */ + public getVisitorProfiles(): VisitorProfile[] { + return Array.from(this.visitorProfiles.values()); + } + + /** + * Get profile for specific IP + */ + public getVisitorProfile(ip: string): VisitorProfile | undefined { + return this.visitorProfiles.get(ip); + } +} + +// Export singleton instance +export const marketingIntelligenceService = new MarketingIntelligenceService(); +export default marketingIntelligenceService; \ No newline at end of file diff --git a/src/lib/nginxLogParser.ts b/src/lib/nginxLogParser.ts new file mode 100644 index 0000000..c18635c --- /dev/null +++ b/src/lib/nginxLogParser.ts @@ -0,0 +1,450 @@ +// NGINX Log Parser for Analytics Dashboard +export interface NGINXLogEntry { + timestamp: string; + ipAddress: string; + method: string; + path: string; + statusCode: number; + bytesSent: number; + referer: string; + userAgent: string; + requestTime: number; + upstreamResponseTime: number; + acceptLanguage: string; + acceptEncoding: string; + connection: string; + upgrade: string; + secFetchDest: string; + secFetchMode: string; + secFetchSite: string; + secFetchUser: string; +} + +export interface NGINXLogSummary { + totalRequests: number; + uniqueIPs: Set; + statusCodes: Record; + topPaths: Record; + topUserAgents: Record; + topReferers: Record; + averageResponseTime: number; + totalBytesSent: number; + timeRange: { + start: string; + end: string; + }; +} + +class NGINXLogParser { + /** + * Convert UTC timestamp to Eastern Time + */ + private convertToEasternTime(utcTimestamp: string): string { + try { + const utcDate = new Date(utcTimestamp); + const etDate = new Date(utcDate.toLocaleString('en-US', { timeZone: 'America/New_York' })); + + // Format as ISO string but with ET timezone + const year = etDate.getFullYear(); + const month = String(etDate.getMonth() + 1).padStart(2, '0'); + const day = String(etDate.getDate()).padStart(2, '0'); + const hours = String(etDate.getHours()).padStart(2, '0'); + const minutes = String(etDate.getMinutes()).padStart(2, '0'); + const seconds = String(etDate.getSeconds()).padStart(2, '0'); + + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.000-05:00`; + } catch (error) { + console.error('Error converting timestamp to ET:', error); + return utcTimestamp; // Fallback to original timestamp + } + } + + /** + * Parse a single NGINX log line + */ + private parseLogLine(line: string): NGINXLogEntry | null { + try { + // Parse the main log format: combined + tracking format + const regex = /^(\S+) - - \[([^\]]+)\] "([^"]*)" (\d+) (\d+) "([^"]*)" "([^"]*)" "([^"]*)" "([^"]*)" rt=([^ ]+) uct="([^"]*)" uht="([^"]*)" urt="([^"]*)" ua="([^"]*)" us="([^"]*)"$/; + const match = line.match(regex); + + if (!match) { + // Try tracking log format + const trackingRegex = /^(\S+) - - \[([^\]]+)\] "([^"]*)" (\d+) (\d+) "([^"]*)" "([^"]*)" "([^"]*)" ([^ ]+) - ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+)$/; + const trackingMatch = line.match(trackingRegex); + + if (trackingMatch) { + const [ + , + ipAddress, + timestamp, + request, + statusCode, + bytesSent, + referer, + userAgent, + xForwardedFor, + requestTime, + acceptLanguage, + acceptEncoding, + connection, + upgrade, + secFetchDest, + secFetchMode, + secFetchSite, + secFetchUser + ] = trackingMatch; + + // Parse the request line + const requestMatch = request.match(/^(\S+) (\S+) (\S+)$/); + if (!requestMatch) { + return null; + } + + const [, method, path, httpVersion] = requestMatch; + + // Extract real IP from X-Forwarded-For header + let realIP = ipAddress; + if (xForwardedFor && xForwardedFor !== '-') { + // X-Forwarded-For can contain multiple IPs, take the first one + const forwardedIPs = xForwardedFor.split(','); + if (forwardedIPs.length > 0) { + realIP = forwardedIPs[0].trim(); + } + } + + return { + timestamp: this.convertToEasternTime(this.parseTimestamp(timestamp)), + ipAddress: realIP, + method, + path, + statusCode: parseInt(statusCode), + bytesSent: parseInt(bytesSent), + referer: referer === '-' ? '' : referer, + userAgent: userAgent === '-' ? '' : userAgent, + requestTime: parseFloat(requestTime) || 0, + upstreamResponseTime: 0, + acceptLanguage: acceptLanguage === '-' ? '' : acceptLanguage, + acceptEncoding: acceptEncoding === '-' ? '' : acceptEncoding, + connection: connection === '-' ? '' : connection, + upgrade: upgrade === '-' ? '' : upgrade, + secFetchDest: secFetchDest === '-' ? '' : secFetchDest, + secFetchMode: secFetchMode === '-' ? '' : secFetchMode, + secFetchSite: secFetchSite === '-' ? '' : secFetchSite, + secFetchUser: secFetchUser === '-' ? '' : secFetchUser + }; + } + + // Try alternative format for logs without tracking data + const altRegex = /^(\S+) - - \[([^\]]+)\] "([^"]*)" (\d+) (\d+) "([^"]*)" "([^"]*)"$/; + const altMatch = line.match(altRegex); + + if (!altMatch) { + return null; + } + + const [ + , + ipAddress, + timestamp, + request, + statusCode, + bytesSent, + referer, + userAgent + ] = altMatch; + + // Parse the request line + const requestMatch = request.match(/^(\S+) (\S+) (\S+)$/); + if (!requestMatch) { + return null; + } + + const [, method, path, httpVersion] = requestMatch; + + return { + timestamp: this.convertToEasternTime(this.parseTimestamp(timestamp)), + ipAddress, + method, + path, + statusCode: parseInt(statusCode), + bytesSent: parseInt(bytesSent), + referer: referer === '-' ? '' : referer, + userAgent: userAgent === '-' ? '' : userAgent, + requestTime: 0, + upstreamResponseTime: 0, + acceptLanguage: '', + acceptEncoding: '', + connection: '', + upgrade: '', + secFetchDest: '', + secFetchMode: '', + secFetchSite: '', + secFetchUser: '' + }; + } + + const [ + , + ipAddress, + timestamp, + request, + statusCode, + bytesSent, + referer, + userAgent, + xForwardedFor, + xRealIP, + requestTime, + upstreamConnectTime, + upstreamHeaderTime, + upstreamResponseTime, + userAgent2, + userSession + ] = match; + + // Parse the request line + const requestMatch = request.match(/^(\S+) (\S+) (\S+)$/); + if (!requestMatch) { + return null; + } + + const [, method, path, httpVersion] = requestMatch; + + // Extract real IP from X-Forwarded-For header + let realIP = ipAddress || 'unknown'; + if (xForwardedFor && xForwardedFor !== '-') { + // X-Forwarded-For can contain multiple IPs, take the first one + const forwardedIPs = xForwardedFor.split(','); + if (forwardedIPs.length > 0) { + realIP = forwardedIPs[0].trim(); + } + } + + return { + timestamp: this.convertToEasternTime(this.parseTimestamp(timestamp)), + ipAddress: realIP, + method, + path, + statusCode: parseInt(statusCode), + bytesSent: parseInt(bytesSent), + referer: referer === '-' ? '' : referer, + userAgent: userAgent === '-' ? '' : userAgent, + requestTime: parseFloat(requestTime) || 0, + upstreamResponseTime: parseFloat(upstreamResponseTime) || 0, + acceptLanguage: '', + acceptEncoding: '', + connection: '', + upgrade: '', + secFetchDest: '', + secFetchMode: '', + secFetchSite: '', + secFetchUser: '' + }; + } catch (error) { + console.error('Error parsing NGINX log line:', error); + return null; + } + } + + /** + * Parse timestamp from NGINX log format + */ + private parseTimestamp(timestamp: string): string { + // Convert from NGINX format to ISO string + // Format: 25/Jul/2025:15:10:42 +0000 + const match = timestamp.match(/^(\d+)\/(\w+)\/(\d+):(\d+):(\d+):(\d+) ([\+\-]\d{4})$/); + + if (!match) { + return new Date().toISOString(); + } + + const [, day, month, year, hour, minute, second, timezone] = match; + + // Convert month name to number + const monthMap: Record = { + 'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', + 'May': '05', 'Jun': '06', 'Jul': '07', 'Aug': '08', + 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12' + }; + + const monthNum = monthMap[month] || '01'; + const paddedDay = day.padStart(2, '0'); + const paddedHour = hour.padStart(2, '0'); + const paddedMinute = minute.padStart(2, '0'); + const paddedSecond = second.padStart(2, '0'); + + // Create ISO string (assuming UTC for now, we'll convert to ET later) + const utcTimestamp = `${year}-${monthNum}-${paddedDay}T${paddedHour}:${paddedMinute}:${paddedSecond}.000Z`; + + return utcTimestamp; + } + + /** + * Read and parse NGINX log files + */ + public async parseLogFiles(logPaths: string[]): Promise { + const entries: NGINXLogEntry[] = []; + + for (const logPath of logPaths) { + try { + const fs = await import('fs/promises'); + const content = await fs.readFile(logPath, 'utf-8'); + const lines = content.split('\n').filter(line => line.trim()); + + for (const line of lines) { + const entry = this.parseLogLine(line); + if (entry) { + entries.push(entry); + } + } + } catch (error) { + console.error(`Error reading log file ${logPath}:`, error); + } + } + + return entries; + } + + /** + * Generate summary statistics from log entries + */ + public generateSummary(entries: NGINXLogEntry[]): NGINXLogSummary { + const summary: NGINXLogSummary = { + totalRequests: entries.length, + uniqueIPs: new Set(), + statusCodes: {}, + topPaths: {}, + topUserAgents: {}, + topReferers: {}, + averageResponseTime: 0, + totalBytesSent: 0, + timeRange: { + start: '', + end: '' + } + }; + + let totalResponseTime = 0; + let validResponseTimes = 0; + + for (const entry of entries) { + // Count unique IPs + summary.uniqueIPs.add(entry.ipAddress); + + // Count status codes + summary.statusCodes[entry.statusCode] = (summary.statusCodes[entry.statusCode] || 0) + 1; + + // Count paths + summary.topPaths[entry.path] = (summary.topPaths[entry.path] || 0) + 1; + + // Count user agents + if (entry.userAgent) { + const browser = this.extractBrowser(entry.userAgent); + summary.topUserAgents[browser] = (summary.topUserAgents[browser] || 0) + 1; + } + + // Count referers + if (entry.referer) { + const domain = this.extractDomain(entry.referer); + summary.topReferers[domain] = (summary.topReferers[domain] || 0) + 1; + } + + // Calculate response times + if (entry.requestTime > 0) { + totalResponseTime += entry.requestTime; + validResponseTimes++; + } + + // Sum bytes sent + summary.totalBytesSent += entry.bytesSent; + + // Track time range + if (!summary.timeRange.start || entry.timestamp < summary.timeRange.start) { + summary.timeRange.start = entry.timestamp; + } + if (!summary.timeRange.end || entry.timestamp > summary.timeRange.end) { + summary.timeRange.end = entry.timestamp; + } + } + + // Calculate average response time + if (validResponseTimes > 0) { + summary.averageResponseTime = totalResponseTime / validResponseTimes; + } + + return summary; + } + + /** + * Extract browser name from user agent + */ + private extractBrowser(userAgent: string): string { + const ua = userAgent.toLowerCase(); + + if (ua.includes('chrome')) return 'Chrome'; + if (ua.includes('firefox')) return 'Firefox'; + if (ua.includes('safari')) return 'Safari'; + if (ua.includes('edge')) return 'Edge'; + if (ua.includes('opera')) return 'Opera'; + if (ua.includes('bot') || ua.includes('crawler')) return 'Bot'; + + return 'Other'; + } + + /** + * Extract domain from referer URL + */ + private extractDomain(referer: string): string { + try { + const url = new URL(referer); + return url.hostname; + } catch { + return 'Direct'; + } + } + + /** + * Get top N items from a record + */ + public getTopItems(record: Record, limit: number = 10): Array<{ key: string; count: number }> { + return Object.entries(record) + .sort(([, a], [, b]) => b - a) + .slice(0, limit) + .map(([key, count]) => ({ key, count })); + } + + /** + * Filter entries by date range + */ + public filterByDateRange(entries: NGINXLogEntry[], startDate: string, endDate: string): NGINXLogEntry[] { + const start = new Date(startDate + 'T00:00:00.000Z'); + const end = new Date(endDate + 'T23:59:59.999Z'); + + const filtered = entries.filter(entry => { + const entryDate = new Date(entry.timestamp); + const isInRange = entryDate >= start && entryDate <= end; + + return isInRange; + }); + + return filtered; + } + + /** + * Get entries for a specific IP address + */ + public getEntriesByIP(entries: NGINXLogEntry[], ipAddress: string): NGINXLogEntry[] { + return entries.filter(entry => entry.ipAddress === ipAddress); + } + + /** + * Get entries for a specific path + */ + public getEntriesByPath(entries: NGINXLogEntry[], path: string): NGINXLogEntry[] { + return entries.filter(entry => entry.path === path); + } +} + +export const nginxLogParser = new NGINXLogParser(); +export default nginxLogParser; \ No newline at end of file