Revert "update"

This reverts commit 75cc618c2b.
This commit is contained in:
pacnpal
2025-09-21 20:11:00 -04:00
parent 75cc618c2b
commit 540f40e689
610 changed files with 4812 additions and 1715 deletions

View File

@@ -1,17 +0,0 @@
"""
Core middleware package.
This package contains middleware components for the Django application,
including view tracking and other core functionality.
"""
from .view_tracking import ViewTrackingMiddleware, get_view_stats_for_content
from .analytics import PgHistoryContextMiddleware
from .nextjs import APIResponseMiddleware
__all__ = [
"ViewTrackingMiddleware",
"get_view_stats_for_content",
"PgHistoryContextMiddleware",
"APIResponseMiddleware",
]

View File

@@ -1,45 +0,0 @@
"""
Analytics and tracking middleware for Django application.
"""
import pghistory
from django.contrib.auth.models import AnonymousUser
from django.core.handlers.wsgi import WSGIRequest
class RequestContextProvider(pghistory.context):
"""Custom context provider for pghistory that extracts information from the request."""
def __call__(self, request: WSGIRequest) -> dict:
return {
"user": (
str(request.user)
if request.user and not isinstance(request.user, AnonymousUser)
else None
),
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
"session_key": (
request.session.session_key if hasattr(request, "session") else None
),
}
# Initialize the context provider
request_context = RequestContextProvider()
class PgHistoryContextMiddleware:
"""
Middleware that ensures request object is available to pghistory context.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Set the pghistory context with request information
context_data = request_context(request)
with pghistory.context(**context_data):
response = self.get_response(request)
return response

View File

@@ -1,48 +0,0 @@
# backend/apps/core/middleware.py
from django.utils.deprecation import MiddlewareMixin
class APIResponseMiddleware(MiddlewareMixin):
"""
Middleware to ensure consistent API responses for Next.js
"""
def process_response(self, request, response):
# Only process API requests
if not request.path.startswith("/api/"):
return response
# Ensure CORS headers are set
if not response.has_header("Access-Control-Allow-Origin"):
origin = request.META.get("HTTP_ORIGIN")
# Allow localhost/127.0.0.1 (any port) and IPv6 loopback for development
if origin:
import re
# support http or https, IPv4 and IPv6 loopback, any port
localhost_pattern = r"^https?://(localhost|127\.0\.0\.1|\[::1\]):\d+"
if re.match(localhost_pattern, origin):
response["Access-Control-Allow-Origin"] = origin
# Ensure caches vary by Origin
existing_vary = response.get("Vary")
if existing_vary:
response["Vary"] = f"{existing_vary}, Origin"
else:
response["Vary"] = "Origin"
# Helpful dev CORS headers (adjust for your frontend requests)
response["Access-Control-Allow-Methods"] = (
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
)
response["Access-Control-Allow-Headers"] = (
"Authorization, Content-Type, X-Requested-With"
)
# Uncomment if your dev frontend needs to send cookies/auth credentials
# response['Access-Control-Allow-Credentials'] = 'true'
else:
response["Access-Control-Allow-Origin"] = "null"
return response

View File

@@ -1,308 +0,0 @@
"""
Performance monitoring middleware for tracking request metrics.
"""
import time
import logging
from django.db import connection
from django.utils.deprecation import MiddlewareMixin
from django.conf import settings
performance_logger = logging.getLogger("performance")
logger = logging.getLogger(__name__)
class PerformanceMiddleware(MiddlewareMixin):
"""Middleware to collect performance metrics for each request"""
def process_request(self, request):
"""Initialize performance tracking for the request"""
request._performance_start_time = time.time()
request._performance_initial_queries = (
len(connection.queries) if hasattr(connection, "queries") else 0
)
def process_response(self, request, response):
"""Log performance metrics after response is ready"""
# Skip performance tracking for certain paths
skip_paths = [
"/health/",
"/admin/jsi18n/",
"/static/",
"/media/",
"/__debug__/",
]
if any(request.path.startswith(path) for path in skip_paths):
return response
# Calculate metrics
end_time = time.time()
start_time = getattr(request, "_performance_start_time", end_time)
duration = end_time - start_time
initial_queries = getattr(request, "_performance_initial_queries", 0)
total_queries = (
len(connection.queries) - initial_queries
if hasattr(connection, "queries")
else 0
)
# Get content length
content_length = 0
if hasattr(response, "content"):
content_length = len(response.content)
elif hasattr(response, "streaming_content"):
# For streaming responses, we can't easily measure content length
content_length = -1
# Build performance data
performance_data = {
"path": request.path,
"method": request.method,
"status_code": response.status_code,
"duration_ms": round(duration * 1000, 2),
"duration_seconds": round(duration, 3),
"query_count": total_queries,
"content_length_bytes": content_length,
"user_id": (
getattr(request.user, "id", None)
if hasattr(request, "user") and request.user.is_authenticated
else None
),
"user_agent": request.META.get("HTTP_USER_AGENT", "")[
:100
], # Truncate user agent
"remote_addr": self._get_client_ip(request),
}
# Add query details in debug mode
if settings.DEBUG and hasattr(connection, "queries") and total_queries > 0:
recent_queries = connection.queries[-total_queries:]
performance_data["queries"] = [
{
"sql": (
query["sql"][:200] + "..."
if len(query["sql"]) > 200
else query["sql"]
),
"time": float(query["time"]),
}
for query in recent_queries[-10:] # Last 10 queries only
]
# Identify slow queries
slow_queries = [q for q in recent_queries if float(q["time"]) > 0.1]
if slow_queries:
performance_data["slow_query_count"] = len(slow_queries)
performance_data["slowest_query_time"] = max(
float(q["time"]) for q in slow_queries
)
# Determine log level based on performance
log_level = self._get_log_level(duration, total_queries, response.status_code)
# Log the performance data
performance_logger.log(
log_level,
f"Request performance: {request.method} {request.path} - "
f"{duration:.3f}s, {total_queries} queries, {response.status_code}",
extra=performance_data,
)
# Add performance headers for debugging (only in debug mode)
if settings.DEBUG:
response["X-Response-Time"] = f"{duration * 1000:.2f}ms"
response["X-Query-Count"] = str(total_queries)
if total_queries > 0 and hasattr(connection, "queries"):
total_query_time = sum(
float(q["time"]) for q in connection.queries[-total_queries:]
)
response["X-Query-Time"] = f"{total_query_time * 1000:.2f}ms"
return response
def process_exception(self, request, exception):
"""Log performance data even when an exception occurs"""
end_time = time.time()
start_time = getattr(request, "_performance_start_time", end_time)
duration = end_time - start_time
initial_queries = getattr(request, "_performance_initial_queries", 0)
total_queries = (
len(connection.queries) - initial_queries
if hasattr(connection, "queries")
else 0
)
performance_data = {
"path": request.path,
"method": request.method,
"status_code": 500, # Exception occurred
"duration_ms": round(duration * 1000, 2),
"query_count": total_queries,
"exception": str(exception),
"exception_type": type(exception).__name__,
"user_id": (
getattr(request.user, "id", None)
if hasattr(request, "user") and request.user.is_authenticated
else None
),
}
performance_logger.error(
f"Request exception: {request.method} {request.path} - "
f"{duration:.3f}s, {total_queries} queries, {type(exception).__name__}: {
exception
}",
extra=performance_data,
)
# Don't return anything - let the exception propagate normally
def _get_client_ip(self, request):
"""Extract client IP address from request"""
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
ip = x_forwarded_for.split(",")[0].strip()
else:
ip = request.META.get("REMOTE_ADDR", "")
return ip
def _get_log_level(self, duration, query_count, status_code):
"""Determine appropriate log level based on performance metrics"""
# Error responses
if status_code >= 500:
return logging.ERROR
elif status_code >= 400:
return logging.WARNING
# Performance-based log levels
if duration > 5.0: # Very slow requests
return logging.ERROR
elif duration > 2.0 or query_count > 20: # Slow requests or high query count
return logging.WARNING
elif duration > 1.0 or query_count > 10: # Moderately slow
return logging.INFO
else:
return logging.DEBUG
class QueryCountMiddleware(MiddlewareMixin):
"""Middleware to track and limit query counts per request"""
def __init__(self, get_response):
self.get_response = get_response
self.query_limit = getattr(settings, "MAX_QUERIES_PER_REQUEST", 50)
super().__init__(get_response)
def process_request(self, request):
"""Initialize query tracking"""
request._query_count_start = (
len(connection.queries) if hasattr(connection, "queries") else 0
)
def process_response(self, request, response):
"""Check query count and warn if excessive"""
if not hasattr(connection, "queries"):
return response
start_count = getattr(request, "_query_count_start", 0)
current_count = len(connection.queries)
request_query_count = current_count - start_count
if request_query_count > self.query_limit:
logger.warning(
f"Excessive query count: {request.path} executed {
request_query_count
} queries "
f"(limit: {self.query_limit})",
extra={
"path": request.path,
"method": request.method,
"query_count": request_query_count,
"query_limit": self.query_limit,
"excessive_queries": True,
},
)
return response
class DatabaseConnectionMiddleware(MiddlewareMixin):
"""Middleware to monitor database connection health"""
def process_request(self, request):
"""Check database connection at start of request"""
try:
# Simple connection test
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
except Exception as e:
logger.error(
f"Database connection failed at request start: {e}",
extra={
"path": request.path,
"method": request.method,
"database_error": str(e),
},
)
# Don't block the request, let Django handle the database error
def process_response(self, request, response):
"""Close database connections properly"""
try:
from django.db import connection
connection.close()
except Exception as e:
logger.warning(f"Error closing database connection: {e}")
return response
class CachePerformanceMiddleware(MiddlewareMixin):
"""Middleware to monitor cache performance"""
def process_request(self, request):
"""Initialize cache performance tracking"""
request._cache_hits = 0
request._cache_misses = 0
request._cache_start_time = time.time()
def process_response(self, request, response):
"""Log cache performance metrics"""
cache_duration = time.time() - getattr(
request, "_cache_start_time", time.time()
)
cache_hits = getattr(request, "_cache_hits", 0)
cache_misses = getattr(request, "_cache_misses", 0)
if cache_hits + cache_misses > 0:
hit_rate = (cache_hits / (cache_hits + cache_misses)) * 100
cache_data = {
"path": request.path,
"cache_hits": cache_hits,
"cache_misses": cache_misses,
"cache_hit_rate": round(hit_rate, 2),
"cache_operations": cache_hits + cache_misses,
# milliseconds
"cache_duration": round(cache_duration * 1000, 2),
}
# Log cache performance
if hit_rate < 50 and cache_hits + cache_misses > 5:
logger.warning(
f"Low cache hit rate for {request.path}: {hit_rate:.1f}%",
extra=cache_data,
)
else:
logger.debug(
f"Cache performance for {request.path}: {hit_rate:.1f}% hit rate",
extra=cache_data,
)
return response

View File

@@ -1,138 +0,0 @@
"""
Request logging middleware for comprehensive request/response logging.
Logs all HTTP requests with detailed data for debugging and monitoring.
"""
import logging
import time
import json
from django.utils.deprecation import MiddlewareMixin
logger = logging.getLogger('request_logging')
class RequestLoggingMiddleware(MiddlewareMixin):
"""
Middleware to log all HTTP requests with method, path, and response code.
Includes detailed request/response data logging for all requests.
"""
# Paths to exclude from detailed logging (e.g., static files, health checks)
EXCLUDE_DETAILED_LOGGING_PATHS = [
'/static/',
'/media/',
'/favicon.ico',
'/health/',
'/admin/jsi18n/',
]
def _should_log_detailed(self, request):
"""Determine if detailed logging should be enabled for this request."""
return not any(
path in request.path for path in self.EXCLUDE_DETAILED_LOGGING_PATHS)
def process_request(self, request):
"""Store request start time and capture request data for detailed logging."""
request._start_time = time.time()
# Enable detailed logging for all requests except excluded paths
should_log_detailed = self._should_log_detailed(request)
request._log_request_data = should_log_detailed
if should_log_detailed:
try:
# Log request data
request_data = {}
if hasattr(request, 'data') and request.data:
request_data = dict(request.data)
elif request.body:
try:
request_data = json.loads(request.body.decode('utf-8'))
except (json.JSONDecodeError, UnicodeDecodeError):
request_data = {'body': str(request.body)[
:200] + '...' if len(str(request.body)) > 200 else str(request.body)}
# Log query parameters
query_params = dict(request.GET) if request.GET else {}
logger.info(f"REQUEST DATA for {request.method} {request.path}:")
if request_data:
logger.info(f" Body: {self._safe_log_data(request_data)}")
if query_params:
logger.info(f" Query: {query_params}")
if hasattr(request, 'user') and request.user.is_authenticated:
logger.info(
f" User: {request.user.username} (ID: {request.user.id})")
except Exception as e:
logger.warning(f"Failed to log request data: {e}")
return None
def process_response(self, request, response):
"""Log request details after response is generated."""
try:
# Calculate request duration
duration = 0
if hasattr(request, '_start_time'):
duration = time.time() - request._start_time
# Basic request logging
logger.info(
f"{request.method} {request.get_full_path()} -> {response.status_code} "
f"({duration:.3f}s)"
)
# Detailed response logging for specific endpoints
if getattr(request, '_log_request_data', False):
try:
# Log response data
if hasattr(response, 'data'):
logger.info(
f"RESPONSE DATA for {request.method} {request.path}:")
logger.info(f" Status: {response.status_code}")
logger.info(f" Data: {self._safe_log_data(response.data)}")
elif hasattr(response, 'content'):
try:
content = json.loads(response.content.decode('utf-8'))
logger.info(
f"RESPONSE DATA for {request.method} {request.path}:")
logger.info(f" Status: {response.status_code}")
logger.info(f" Content: {self._safe_log_data(content)}")
except (json.JSONDecodeError, UnicodeDecodeError):
logger.info(
f"RESPONSE DATA for {request.method} {request.path}:")
logger.info(f" Status: {response.status_code}")
logger.info(f" Content: {str(response.content)[:200]}...")
except Exception as e:
logger.warning(f"Failed to log response data: {e}")
except Exception:
# Don't let logging errors break the request
pass
return response
def _safe_log_data(self, data):
"""Safely log data, truncating if too large and masking sensitive fields."""
try:
# Convert to string representation
if isinstance(data, dict):
# Mask sensitive fields
safe_data = {}
for key, value in data.items():
if any(sensitive in key.lower() for sensitive in ['password', 'token', 'secret', 'key']):
safe_data[key] = '***MASKED***'
else:
safe_data[key] = value
data_str = json.dumps(safe_data, indent=2, default=str)
else:
data_str = json.dumps(data, indent=2, default=str)
# Truncate if too long
if len(data_str) > 1000:
return data_str[:1000] + '...[TRUNCATED]'
return data_str
except Exception:
return str(data)[:500] + '...[ERROR_LOGGING]'

View File

@@ -1,329 +0,0 @@
"""
View Tracking Middleware for automatic PageView recording.
This middleware automatically tracks page views for park and ride pages,
implementing IP-based deduplication to prevent spam and provide accurate
analytics for the trending algorithm.
"""
import logging
import re
from datetime import timedelta
from typing import Optional, Union
from django.http import HttpRequest, HttpResponse
from django.utils import timezone
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.conf import settings
from apps.core.analytics import PageView
from apps.parks.models import Park
from apps.rides.models import Ride
# Type alias for content objects
ContentObject = Union[Park, Ride]
logger = logging.getLogger(__name__)
class ViewTrackingMiddleware:
"""
Middleware for tracking page views with IP deduplication.
Automatically creates PageView records when users visit park or ride pages.
Implements 24-hour IP deduplication window to prevent view inflation.
"""
def __init__(self, get_response):
self.get_response = get_response
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
# URL patterns for tracking - matches park and ride detail pages
self.tracked_patterns = [
(r"^/parks/(?P<slug>[\w-]+)/$", "park"),
(r"^/rides/(?P<slug>[\w-]+)/$", "ride"),
# Add API patterns if needed
(r"^/api/v1/parks/(?P<slug>[\w-]+)/$", "park"),
(r"^/api/v1/rides/(?P<slug>[\w-]+)/$", "ride"),
]
# Compile patterns for performance
self.compiled_patterns = [
(re.compile(pattern), content_type)
for pattern, content_type in self.tracked_patterns
]
# Cache configuration
self.cache_timeout = 60 * 15 # 15 minutes
self.dedup_window_hours = 24
def __call__(self, request: HttpRequest) -> HttpResponse:
"""Process the request and track views if applicable."""
response = self.get_response(request)
# Only track successful GET requests
if (
request.method == "GET"
and 200 <= response.status_code < 300
and not self._should_skip_tracking(request)
):
try:
self._track_view_if_applicable(request)
except Exception as e:
# Log error but don't break the request
self.logger.error(f"Error tracking view: {e}", exc_info=True)
return response
def _should_skip_tracking(self, request: HttpRequest) -> bool:
"""Check if this request should be skipped for tracking."""
# Skip if disabled in settings
if not getattr(settings, "ENABLE_VIEW_TRACKING", True):
return True
# Skip requests from bots/crawlers
user_agent = request.META.get("HTTP_USER_AGENT", "").lower()
bot_indicators = [
"bot",
"crawler",
"spider",
"scraper",
"facebook",
"twitter",
"linkedin",
"google",
"bing",
"yahoo",
"duckduckgo",
"slurp",
]
if any(indicator in user_agent for indicator in bot_indicators):
return True
# Skip requests without real IP
if not self._get_client_ip(request):
return True
# Skip AJAX requests (optional - depending on requirements)
if request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest":
return True
return False
def _track_view_if_applicable(self, request: HttpRequest) -> None:
"""Track view if the URL matches tracked patterns."""
path = request.path
for pattern, content_type in self.compiled_patterns:
match = pattern.match(path)
if match:
slug = match.group("slug")
self._record_page_view(request, content_type, slug)
break
def _record_page_view(
self, request: HttpRequest, content_type: str, slug: str
) -> None:
"""Record a page view for the specified content."""
client_ip = self._get_client_ip(request)
if not client_ip:
return
try:
# Get the content object
content_obj = self._get_content_object(content_type, slug)
if not content_obj:
self.logger.warning(
f"Content not found: {content_type} with slug '{slug}'"
)
return
# Check deduplication
if self._is_duplicate_view(content_obj, client_ip):
self.logger.debug(
f"Duplicate view skipped for {content_type} {slug} from {client_ip}"
)
return
# Create PageView record
self._create_page_view(content_obj, client_ip, request)
self.logger.debug(
f"Recorded view for {content_type} {slug} from {client_ip}"
)
except Exception as e:
self.logger.error(
f"Failed to record page view for {content_type} {slug}: {e}"
)
def _get_content_object(
self, content_type: str, slug: str
) -> Optional[ContentObject]:
"""Get the content object by type and slug."""
try:
if content_type == "park":
# Use get_by_slug method to handle historical slugs
park, _ = Park.get_by_slug(slug)
return park
elif content_type == "ride":
# For rides, we need to search by slug within parks
return Ride.objects.filter(slug=slug).first()
else:
self.logger.warning(f"Unknown content type: {content_type}")
return None
except Park.DoesNotExist:
return None
except Exception as e:
self.logger.error(f"Error getting {content_type} with slug {slug}: {e}")
return None
def _is_duplicate_view(self, content_obj: ContentObject, client_ip: str) -> bool:
"""Check if this view is a duplicate within the deduplication window."""
# Use cache for performance
cache_key = self._get_dedup_cache_key(content_obj, client_ip)
if cache.get(cache_key):
return True
# Check database as fallback
content_type = ContentType.objects.get_for_model(content_obj)
cutoff_time = timezone.now() - timedelta(hours=self.dedup_window_hours)
existing_view = PageView.objects.filter(
content_type=content_type,
object_id=content_obj.pk,
ip_address=client_ip,
timestamp__gte=cutoff_time,
).exists()
if not existing_view:
# Set cache to prevent future duplicates
cache.set(cache_key, True, timeout=self.dedup_window_hours * 3600)
return existing_view
def _create_page_view(
self, content_obj: ContentObject, client_ip: str, request: HttpRequest
) -> None:
"""Create a new PageView record."""
content_type = ContentType.objects.get_for_model(content_obj)
# Extract additional metadata
user_agent = request.META.get("HTTP_USER_AGENT", "")[
:500
] # Truncate long user agents
referer = request.META.get("HTTP_REFERER", "")[:500]
PageView.objects.create(
content_type=content_type,
object_id=content_obj.pk,
ip_address=client_ip,
user_agent=user_agent,
referer=referer,
path=request.path[:500],
)
# Update cache for deduplication
cache_key = self._get_dedup_cache_key(content_obj, client_ip)
cache.set(cache_key, True, timeout=self.dedup_window_hours * 3600)
def _get_dedup_cache_key(self, content_obj: ContentObject, client_ip: str) -> str:
"""Generate cache key for deduplication."""
content_type = ContentType.objects.get_for_model(content_obj)
return f"pageview_dedup:{content_type.id}:{content_obj.pk}:{client_ip}"
def _get_client_ip(self, request: HttpRequest) -> Optional[str]:
"""Extract client IP address from request."""
# Check for forwarded IP (common in production with load balancers)
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
# Take the first IP in the chain (client IP)
ip = x_forwarded_for.split(",")[0].strip()
if self._is_valid_ip(ip):
return ip
# Check for real IP header (some proxy configurations)
x_real_ip = request.META.get("HTTP_X_REAL_IP")
if x_real_ip and self._is_valid_ip(x_real_ip):
return x_real_ip
# Fall back to remote address
remote_addr = request.META.get("REMOTE_ADDR")
if remote_addr and self._is_valid_ip(remote_addr):
return remote_addr
return None
def _is_valid_ip(self, ip: str) -> bool:
"""Validate IP address format."""
try:
# Basic validation - check if it looks like an IP
parts = ip.split(".")
if len(parts) != 4:
return False
for part in parts:
if not part.isdigit() or not 0 <= int(part) <= 255:
return False
# Skip localhost and private IPs in production
if getattr(settings, "SKIP_LOCAL_IPS", not settings.DEBUG):
if ip.startswith(("127.", "192.168.", "10.")) or ip.startswith("172."):
if any(
16 <= int(ip.split(".")[1]) <= 31
for _ in [ip]
if ip.startswith("172.")
):
return False
return True
except (ValueError, IndexError):
return False
def get_view_stats_for_content(content_obj: ContentObject, hours: int = 24) -> dict:
"""
Helper function to get view statistics for content.
Args:
content_obj: The content object (Park or Ride)
hours: Time window in hours for stats
Returns:
Dictionary with view statistics
"""
try:
content_type = ContentType.objects.get_for_model(content_obj)
cutoff_time = timezone.now() - timedelta(hours=hours)
total_views = PageView.objects.filter(
content_type=content_type,
object_id=content_obj.pk,
timestamp__gte=cutoff_time,
).count()
unique_views = (
PageView.objects.filter(
content_type=content_type,
object_id=content_obj.pk,
timestamp__gte=cutoff_time,
)
.values("ip_address")
.distinct()
.count()
)
return {
"total_views": total_views,
"unique_views": unique_views,
"hours": hours,
"content_type": content_type.model,
"content_id": content_obj.pk,
}
except Exception as e:
logger.error(f"Error getting view stats: {e}")
return {"total_views": 0, "unique_views": 0, "hours": hours, "error": str(e)}