Comprehensive development guide for the Open Source Site Tracking platform, covering setup, architecture, and best practices.
git clone https://github.com/AutoBotSolutions/Opensource-Site-Tracking.git
cd opensource-site-tracking
# Create virtual environment
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
cd backend
pip install -r requirements.txt
pip install -r requirements-dev.txt
# Initialize database
python init_db.py
# Install dependencies
cd frontend
npm install
# Start development server
npm run dev
# Development server
cd backend
python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000
The platform follows a modern microservices-inspired architecture within a monolithic structure:
opensource-site-tracking/
├── backend/
│ ├── app/
│ │ ├── __init__.py
│ │ ├── models/ # Database models
│ │ ├── routes/ # API endpoints
│ │ ├── services/ # Business logic
│ │ ├── utils/ # Helper functions
│ │ └── config/ # Configuration
│ ├── tests/ # Test suite
│ ├── migrations/ # Database migrations
│ ├── requirements.txt # Dependencies
│ └── main.py # Application entry
├── frontend/
│ ├── components/ # React components
│ ├── pages/ # Page components
│ ├── hooks/ # Custom hooks
│ ├── services/ # API services
│ ├── utils/ # Utility functions
│ ├── styles/ # Styling
│ ├── package.json # Dependencies
│ └── next.config.js # Next.js config
├── docs/ # Documentation
├── docker-compose.yml # Docker configuration
└── README.md # Project README
# backend/app/routes/analytics.py
from fastapi import APIRouter, Depends
from app.services.analytics import AnalyticsService
router = APIRouter(prefix="/analytics", tags=["analytics"])
@router.get("/{project_id}/overview")
async def get_analytics_overview(
project_id: str,
analytics_service: AnalyticsService = Depends()
):
"""Get analytics overview for project"""
return await analytics_service.get_overview(project_id)
@router.get("/{project_id}/pageviews")
async def get_page_views(
project_id: str,
start_date: str,
end_date: str,
analytics_service: AnalyticsService = Depends()
):
"""Get page views analytics"""
return await analytics_service.get_page_views(
project_id, start_date, end_date
)
# backend/app/services/analytics.py
from typing import Dict, List, Optional
from datetime import datetime
from app.models.analytics import AnalyticsData
class AnalyticsService:
def __init__(self, db_session):
self.db = db_session
async def get_overview(self, project_id: str) -> Dict:
"""Get analytics overview"""
page_views = await self.get_page_views_count(project_id)
unique_visitors = await self.get_unique_visitors_count(project_id)
return {
"page_views": page_views,
"unique_visitors": unique_visitors,
"average_session_duration": await self.get_avg_session_duration(project_id),
"bounce_rate": await self.get_bounce_rate(project_id)
}
async def get_page_views_count(self, project_id: str) -> int:
"""Get total page views for project"""
return self.db.query(AnalyticsData).filter(
AnalyticsData.project_id == project_id,
AnalyticsData.event_type == "page_view"
).count()
# backend/app/models/analytics.py
from sqlalchemy import Column, String, Integer, DateTime, Text, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from datetime import datetime
Base = declarative_base()
class Project(Base):
__tablename__ = "projects"
id = Column(String, primary_key=True)
name = Column(String, nullable=False)
description = Column(Text)
repository_url = Column(String)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
analytics = relationship("AnalyticsData", back_populates="project")
class AnalyticsData(Base):
__tablename__ = "analytics_data"
id = Column(Integer, primary_key=True)
project_id = Column(String, ForeignKey("projects.id"))
event_type = Column(String, nullable=False)
page_url = Column(String)
user_id = Column(String)
session_id = Column(String)
properties = Column(Text) # JSON string
timestamp = Column(DateTime, default=datetime.utcnow)
project = relationship("Project", back_populates="analytics")
# backend/app/config.py
import os
from typing import Optional
class Config:
# Database
DATABASE_URL: str = os.environ.get("DATABASE_URL", "sqlite:///site_tracking.db")
# Security
SECRET_KEY: str = os.environ.get("SECRET_KEY", "dev-secret-key")
JWT_SECRET_KEY: str = os.environ.get("JWT_SECRET_KEY", "jwt-secret-key")
JWT_EXPIRATION_HOURS: int = int(os.environ.get("JWT_EXPIRATION_HOURS", "24"))
# Application
DEBUG: bool = os.environ.get("DEBUG", "False").lower() == "true"
ENVIRONMENT: str = os.environ.get("ENVIRONMENT", "development")
# Analytics
ANALYTICS_ENABLED: bool = os.environ.get("ANALYTICS_ENABLED", "True").lower() == "true"
DATA_RETENTION_DAYS: int = int(os.environ.get("DATA_RETENTION_DAYS", "365"))
class DevelopmentConfig(Config):
DEBUG = True
ENVIRONMENT = "development"
class ProductionConfig(Config):
DEBUG = False
ENVIRONMENT = "production"
config = {
"development": DevelopmentConfig,
"production": ProductionConfig
}
// frontend/components/AnalyticsDashboard.tsx
import React, { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
interface AnalyticsData {
date: string;
pageViews: number;
uniqueVisitors: number;
}
const AnalyticsDashboard: React.FC = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchAnalyticsData();
}, []);
const fetchAnalyticsData = async () => {
try {
const response = await fetch('/api/analytics/overview');
const analyticsData = await response.json();
setData(analyticsData);
} catch (error) {
console.error('Error fetching analytics:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return Loading...;
}
return (
Analytics Overview
);
};
export default AnalyticsDashboard;
// frontend/hooks/useAnalytics.ts
import { useState, useEffect } from 'react';
interface AnalyticsData {
pageViews: number;
uniqueVisitors: number;
averageSessionDuration: number;
bounceRate: number;
}
export const useAnalytics = (projectId: string) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(`/api/analytics/${projectId}/overview`);
if (!response.ok) {
throw new Error('Failed to fetch analytics');
}
const analyticsData = await response.json();
setData(analyticsData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchData();
}, [projectId]);
return { data, loading, error };
};
// frontend/services/analytics.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
export class AnalyticsService {
private static async request(endpoint: string, options: RequestInit = {}) {
const url = `${API_BASE_URL}/api/v1${endpoint}`;
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
static async getOverview(projectId: string) {
return this.request(`/analytics/${projectId}/overview`);
}
static async getPageViews(projectId: string, startDate: string, endDate: string) {
return this.request(`/analytics/${projectId}/pageviews?start_date=${startDate}&end_date=${endDate}`);
}
static async getVisitors(projectId: string, startDate: string, endDate: string) {
return this.request(`/analytics/${projectId}/visitors?start_date=${startDate}&end_date=${endDate}`);
}
}
# backend/tests/test_analytics.py
import pytest
from app.services.analytics import AnalyticsService
from app.models.analytics import Project, AnalyticsData
class TestAnalyticsService:
def setup_method(self):
self.analytics_service = AnalyticsService(db_session)
self.project = Project(
id="test-project",
name="Test Project"
)
db_session.add(self.project)
db_session.commit()
def test_get_overview(self):
# Create test data
analytics_data = AnalyticsData(
project_id=self.project.id,
event_type="page_view",
page_url="/test"
)
db_session.add(analytics_data)
db_session.commit()
# Test service method
result = self.analytics_service.get_overview(self.project.id)
assert "page_views" in result
assert "unique_visitors" in result
assert result["page_views"] >= 1
def test_get_page_views_count(self):
# Create test data
for i in range(5):
analytics_data = AnalyticsData(
project_id=self.project.id,
event_type="page_view",
page_url=f"/test{i}"
)
db_session.add(analytics_data)
db_session.commit()
# Test service method
count = self.analytics_service.get_page_views_count(self.project.id)
assert count == 5
// frontend/components/__tests__/AnalyticsDashboard.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import AnalyticsDashboard from '../AnalyticsDashboard';
// Mock fetch
global.fetch = jest.fn();
describe('AnalyticsDashboard', () => {
beforeEach(() => {
(fetch as jest.Mock).mockClear();
});
it('renders loading state', () => {
(fetch as jest.Mock).mockImplementationOnce(() =>
new Promise(() => {})
);
render( );
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('renders analytics data', async () => {
const mockData = [
{ date: '2024-01-01', pageViews: 100, uniqueVisitors: 50 },
{ date: '2024-01-02', pageViews: 120, uniqueVisitors: 60 }
];
(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockData,
});
render( );
await waitFor(() => {
expect(screen.getByText('Analytics Overview')).toBeInTheDocument();
});
});
it('handles fetch error', async () => {
(fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'));
render( );
await waitFor(() => {
expect(screen.getByText('Error: Network error')).toBeInTheDocument();
});
});
});
# backend/tests/test_api.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
class TestAnalyticsAPI:
def test_get_analytics_overview(self):
response = client.get("/api/v1/analytics/test-project/overview")
assert response.status_code == 200
data = response.json()
assert "page_views" in data
assert "unique_visitors" in data
def test_get_analytics_overview_invalid_project(self):
response = client.get("/api/v1/analytics/invalid-project/overview")
assert response.status_code == 404
def test_track_event(self):
event_data = {
"event_type": "page_view",
"page_url": "/test",
"properties": {"test": "value"}
}
response = client.post("/api/v1/events/test-project", json=event_data)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
# backend/Dockerfile
FROM python:3.9-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Expose port
EXPOSE 8000
# Run application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
version: '3.8'
services:
backend:
build: ./backend
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/site_tracking
- SECRET_KEY=your-secret-key
depends_on:
- db
volumes:
- ./backend:/app
frontend:
build: ./frontend
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_API_URL=http://localhost:8000
depends_on:
- backend
volumes:
- ./frontend:/app
db:
image: postgres:13
environment:
- POSTGRES_DB=site_tracking
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
# Production environment variables
DATABASE_URL=postgresql://user:password@localhost:5432/site_tracking
SECRET_KEY=your-production-secret-key
JWT_SECRET_KEY=your-production-jwt-secret
DEBUG=False
ENVIRONMENT=production
ANALYTICS_ENABLED=True
DATA_RETENTION_DAYS=365
# Gunicorn configuration
gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:8000
# Systemd service
[Unit]
Description=Open Source Site Tracking Backend
After=network.target
[Service]
Type=exec
User=www-data
Group=www-data
WorkingDirectory=/opt/site-tracking/backend
ExecStart=/opt/site-tracking/venv/bin/gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:8000
Restart=always
[Install]
WantedBy=multi-user.target