From 8603690e482004a873cd66f865f2f7eb212b7b94 Mon Sep 17 00:00:00 2001 From: "Abdulmalik A." Date: Thu, 26 Mar 2026 22:35:50 +0100 Subject: [PATCH] Revert "Implemented User Activity Tracking" --- .github/workflows/ci-cd.yml | 2 - IMPLEMENTATION_SUMMARY.md | 327 ------------ backend/.env.example | 17 - backend/DEPLOYMENT_CHECKLIST.md | 327 ------------ backend/scripts/create-analytics-tables.sql | 124 ----- backend/src/analytics/QUICKSTART.md | 184 ------- backend/src/analytics/README.md | 464 ------------------ backend/src/analytics/analytics.module.ts | 43 -- .../controllers/analytics.controller.ts | 156 ------ backend/src/analytics/entities/index.ts | 3 - .../src/analytics/entities/metrics.entity.ts | 62 --- .../src/analytics/entities/session.entity.ts | 93 ---- .../entities/user-activity.entity.ts | 164 ------- .../middleware/activity-tracker.middleware.ts | 298 ----------- .../analytics/providers/activity.service.ts | 249 ---------- .../providers/analytics-db.service.ts | 64 --- .../providers/data-retention.service.ts | 39 -- .../analytics/providers/metrics.service.ts | 344 ------------- .../providers/privacy-preferences.service.ts | 87 ---- .../src/analytics/utils/data-anonymizer.ts | 145 ------ backend/src/app.module.ts | 10 +- backend/src/config/analytics.config.ts | 20 - middleware/src/index.ts | 4 - 23 files changed, 1 insertion(+), 3225 deletions(-) delete mode 100644 IMPLEMENTATION_SUMMARY.md delete mode 100644 backend/DEPLOYMENT_CHECKLIST.md delete mode 100644 backend/scripts/create-analytics-tables.sql delete mode 100644 backend/src/analytics/QUICKSTART.md delete mode 100644 backend/src/analytics/README.md delete mode 100644 backend/src/analytics/analytics.module.ts delete mode 100644 backend/src/analytics/controllers/analytics.controller.ts delete mode 100644 backend/src/analytics/entities/index.ts delete mode 100644 backend/src/analytics/entities/metrics.entity.ts delete mode 100644 backend/src/analytics/entities/session.entity.ts delete mode 100644 backend/src/analytics/entities/user-activity.entity.ts delete mode 100644 backend/src/analytics/middleware/activity-tracker.middleware.ts delete mode 100644 backend/src/analytics/providers/activity.service.ts delete mode 100644 backend/src/analytics/providers/analytics-db.service.ts delete mode 100644 backend/src/analytics/providers/data-retention.service.ts delete mode 100644 backend/src/analytics/providers/metrics.service.ts delete mode 100644 backend/src/analytics/providers/privacy-preferences.service.ts delete mode 100644 backend/src/analytics/utils/data-anonymizer.ts delete mode 100644 backend/src/config/analytics.config.ts diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 0e41cbf..c258e50 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -10,8 +10,6 @@ jobs: steps: - name: Validate PR title uses: amannn/action-semantic-pull-request@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: types: | feat diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index dfb3993..0000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,327 +0,0 @@ -# User Activity Tracking Middleware - Implementation Summary - -## ✅ Implementation Complete - -All requirements from Issue #321 have been successfully implemented. - ---- - -## đŸ“Ļ What Was Built - -### Core Infrastructure (15 files created) - -#### Database Layer -- `user-activity.entity.ts` - Main activity tracking entity -- `session.entity.ts` - Session management entity -- `metrics.entity.ts` - Aggregated metrics storage -- `analytics.config.ts` - Analytics configuration - -#### Services (5 providers) -- `analytics-db.service.ts` - Database connection manager -- `activity.service.ts` - Activity CRUD operations -- `metrics.service.ts` - Metrics calculation engine -- `privacy-preferences.service.ts` - Opt-out management -- `data-retention.service.ts` - Automated cleanup jobs - -#### Middleware & Utilities -- `activity-tracker.middleware.ts` - Core tracking middleware -- `data-anonymizer.ts` - PII removal utilities - -#### API Layer -- `analytics.controller.ts` - REST API endpoints (9 endpoints) -- `analytics.module.ts` - Module configuration - -#### Documentation -- `README.md` - Comprehensive implementation guide -- `QUICKSTART.md` - Developer quick start guide - ---- - -## ✨ Features Delivered - -### Automatic Tracking -✅ User authentication (login, logout, signup) -✅ Puzzle interactions (started, submitted, completed) -✅ Daily quest progress (viewed, progressed, completed, claimed) -✅ Category browsing -✅ Profile updates -✅ Social interactions (friend requests, challenges) -✅ Achievement unlocks -✅ Point redemptions - -### Privacy Compliance (GDPR/CCPA) -✅ IP address anonymization (last octet removed) -✅ No PII logged unnecessarily -✅ Do-Not-Track header support -✅ User opt-out mechanism (Redis-backed) -✅ Data retention limits (90 days auto-delete) -✅ Country/city level only (no precise coordinates) -✅ Consent status tracked - -### Performance Optimizations -✅ Async processing (non-blocking) -✅ <2ms request impact -✅ Redis caching for opt-out status -✅ Separate analytics database option -✅ Batch-ready architecture - -### Analytics API -✅ GET `/analytics/metrics/dau` - Daily Active Users -✅ GET `/analytics/metrics/wau` - Weekly Active Users -✅ GET `/analytics/metrics/session-duration` - Avg session duration -✅ GET `/analytics/metrics/feature-usage` - Feature statistics -✅ GET `/analytics/metrics/platform-distribution` - Platform breakdown -✅ GET `/analytics/metrics/device-distribution` - Device breakdown -✅ GET `/analytics/activities` - Recent activities -✅ GET `/analytics/activities/:userId` - User-specific activities -✅ POST `/analytics/activities/query` - Advanced filtering - -### Data Structure -```typescript -{ - userId?: string, // Optional for anonymous - sessionId: string, // Required - eventType: EventType, // Category of event - eventCategory: EventCategory, // Specific action - timestamp: Date, - duration: number, // Milliseconds - metadata: object, // Sanitized JSONB - device: { browser, os, type }, - platform: 'web' | 'mobile' | 'pwa', - geolocation: { country, city }, - anonymizedIp: string, - userAgent: string, - referrer: string, - isAnonymous: boolean, - consentStatus: 'opted-in' | 'opted-out' | 'not-set', - dataRetentionExpiry: Date // Auto-cleanup -} -``` - ---- - -## 🚀 Getting Started - -### Quick Setup (5 minutes) - -1. **Add to `.env`:** - ```bash - ANALYTICS_DB_AUTOLOAD=true - ANALYTICS_DB_SYNC=true - ANALYTICS_DATA_RETENTION_DAYS=90 - RESPECT_DNT_HEADER=true - ``` - -2. **Install dependency:** - ```bash - npm install @nestjs/schedule - ``` - -3. **Restart server:** - ```bash - npm run start:dev - ``` - -That's it! Tracking is now automatic. - -### Test It - -```bash -# View recent activities -curl http://localhost:3000/analytics/activities?limit=10 - -# Get today's DAU -curl http://localhost:3000/analytics/metrics/dau - -# Check Swagger docs -open http://localhost:3000/docs -``` - ---- - -## 📊 Success Criteria Met - -| Requirement | Status | Notes | -|-------------|--------|-------| -| All significant user actions tracked | ✅ | Automatic via middleware | -| Activity data stored asynchronously | ✅ | Non-blocking writes | -| Analytics queryable via API | ✅ | 9 REST endpoints | -| User privacy preferences respected | ✅ | Opt-out honored | -| Anonymous and authenticated tracking | ✅ | Session-based + user ID | -| Data retention policy enforced | ✅ | 90-day auto-delete | -| Real-time dashboard support | ✅ | API ready for WebSocket | -| Historical analytics available | ✅ | Metrics aggregation | -| No unnecessary PII logged | ✅ | Anonymization utilities | -| Performance impact <2ms | ✅ | Async processing | -| GDPR/CCPA compliant | ✅ | Full compliance | -| DNT header respected | ✅ | Configurable | - ---- - -## 🔧 Configuration Options - -### Development (Default) -```bash -ANALYTICS_DB_AUTOLOAD=true -ANALYTICS_DB_SYNC=true -# Uses main database -``` - -### Production -```bash -ANALYTICS_DB_URL=postgresql://user:pass@host:5432/analytics_db -ANALYTICS_DB_HOST=localhost -ANALYTICS_DB_PORT=5433 -ANALYTICS_DB_USER=analytics_user -ANALYTICS_DB_PASSWORD=secure_password -ANALYTICS_DB_NAME=mindblock_analytics -ANALYTICS_DB_SYNC=false -ANALYTICS_DB_AUTOLOAD=true -ANALYTICS_DATA_RETENTION_DAYS=90 -RESPECT_DNT_HEADER=true -TRACKING_OPT_OUT_BY_DEFAULT=false -``` - ---- - -## 📁 File Structure - -``` -backend/src/analytics/ -├── entities/ -│ ├── user-activity.entity.ts -│ ├── session.entity.ts -│ ├── metrics.entity.ts -│ └── index.ts -├── providers/ -│ ├── analytics-db.service.ts -│ ├── activity.service.ts -│ ├── metrics.service.ts -│ ├── privacy-preferences.service.ts -│ └── data-retention.service.ts -├── middleware/ -│ └── activity-tracker.middleware.ts -├── utils/ -│ └── data-anonymizer.ts -├── controllers/ -│ └── analytics.controller.ts -├── analytics.module.ts -├── README.md # Full documentation -└── QUICKSTART.md # Quick start guide - -backend/ -├── .env.example # Updated with analytics config -├── src/config/ -│ └── analytics.config.ts -└── src/app.module.ts # Updated with AnalyticsModule -``` - ---- - -## đŸŽ¯ Next Steps (Optional Enhancements) - -### Phase 2 Candidates - -1. **Real-time Dashboard** (WebSocket Gateway) - - Live active user count - - Real-time activity stream - - Milestone broadcasts - -2. **Advanced Metrics** - - Retention cohorts - - Funnel analysis - - User segmentation - -3. **Export & Reporting** - - CSV/JSON exports - - Scheduled reports - - Email digests - -4. **Enhanced Privacy** - - Opt-out API endpoint - - Data export API - - Deletion request handling - -5. **Performance Monitoring** - - Benchmark suite - - Alerting on failures - - Performance dashboards - ---- - -## 🔍 Testing Checklist - -Before deploying to production: - -- [ ] Verify analytics tables created -- [ ] Test all 9 API endpoints -- [ ] Confirm DNT header respected -- [ ] Test opt-out mechanism -- [ ] Verify data cleanup job runs -- [ ] Check performance impact (<2ms) -- [ ] Review logs for errors -- [ ] Test with separate analytics DB -- [ ] Validate metrics accuracy - ---- - -## 📞 Support - -### Documentation -- **Quick Start**: `backend/src/analytics/QUICKSTART.md` -- **Full Guide**: `backend/src/analytics/README.md` -- **API Docs**: `http://localhost:3000/docs` - -### Common Issues - -**No data appearing?** -- Check `.env` has `ANALYTICS_DB_AUTOLOAD=true` -- Verify TypeORM synced entities -- Check backend logs - -**High latency?** -- Use separate analytics database -- Verify Redis caching enabled -- Check database indexes - -**Want to disable?** -- Set `ANALYTICS_DB_AUTOLOAD=false` -- No code changes needed - ---- - -## 🏆 Key Achievements - -1. **Zero Blocking** - All async processing -2. **Privacy First** - GDPR/CCPA compliant by design -3. **Developer Friendly** - Simple setup, great docs -4. **Production Ready** - Robust error handling -5. **Performant** - <2ms impact target met -6. **Scalable** - Separate DB, caching, batching ready - ---- - -## 📈 Metrics - -- **Files Created**: 18 -- **Lines of Code**: ~2,500 -- **Endpoints**: 9 REST APIs -- **Entities**: 3 TypeORM entities -- **Services**: 5 providers -- **Middleware**: 1 core tracker -- **Documentation**: 2 comprehensive guides - ---- - -**Implementation Date**: March 26, 2026 -**Status**: ✅ Production Ready -**Version**: 1.0.0 -**Issue**: #321 - User Activity Tracking Middleware for Analytics - ---- - -## 🎉 Ready to Deploy! - -The User Activity Tracking Middleware is fully implemented and ready for production use. All acceptance criteria have been met, and the system is designed for scalability, privacy, and performance. - -**Next Action**: Deploy to staging environment for testing. diff --git a/backend/.env.example b/backend/.env.example index 9a2694e..3e53f67 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -27,20 +27,3 @@ MAIL_USER=your-email@gmail.com MAIL_PASSWORD=your-app-password MAIL_FROM_NAME=MindBlock MAIL_FROM_ADDRESS=noreply@mindblock.com - -# Analytics Database (Optional - falls back to main DB if not configured) -ANALYTICS_DB_URL=postgresql://analytics_user:secure_password@localhost:5432/mindblock_analytics -ANALYTICS_DB_HOST=localhost -ANALYTICS_DB_PORT=5433 -ANALYTICS_DB_USER=analytics_user -ANALYTICS_DB_PASSWORD=secure_password -ANALYTICS_DB_NAME=mindblock_analytics -ANALYTICS_DB_SYNC=false -ANALYTICS_DB_AUTOLOAD=true - -# Data Retention -ANALYTICS_DATA_RETENTION_DAYS=90 - -# Privacy Defaults -TRACKING_OPT_OUT_BY_DEFAULT=false -RESPECT_DNT_HEADER=true diff --git a/backend/DEPLOYMENT_CHECKLIST.md b/backend/DEPLOYMENT_CHECKLIST.md deleted file mode 100644 index 7880c60..0000000 --- a/backend/DEPLOYMENT_CHECKLIST.md +++ /dev/null @@ -1,327 +0,0 @@ -# Analytics Deployment Checklist - -## Pre-Deployment - -### Environment Configuration -- [ ] Add analytics environment variables to production `.env` -- [ ] Set up separate analytics database (recommended) -- [ ] Configure database credentials securely -- [ ] Set `ANALYTICS_DB_SYNC=false` in production -- [ ] Verify `ANALYTICS_DATA_RETENTION_DAYS=90` - -### Database Setup -- [ ] Create analytics database: - ```sql - CREATE DATABASE mindblock_analytics; - ``` -- [ ] Create database user: - ```sql - CREATE USER analytics_user WITH PASSWORD 'secure_password'; - GRANT ALL PRIVILEGES ON DATABASE mindblock_analytics TO analytics_user; - ``` -- [ ] Run migration script: - ```bash - psql -U analytics_user -d mindblock_analytics -f backend/scripts/create-analytics-tables.sql - ``` - -### Dependencies -- [ ] Install `@nestjs/schedule`: - ```bash - npm install @nestjs/schedule - ``` -- [ ] Verify all TypeScript dependencies resolved -- [ ] Run `npm install` on production server - ---- - -## Deployment Steps - -### Step 1: Deploy Code -- [ ] Commit all analytics files -- [ ] Push to staging branch -- [ ] Run tests in staging environment -- [ ] Monitor for errors - -### Step 2: Database Migration -- [ ] Execute SQL migration in production -- [ ] Verify tables created successfully -- [ ] Check indexes exist -- [ ] Test database connection - -### Step 3: Environment Variables -- [ ] Set production env vars: - ```bash - ANALYTICS_DB_URL=postgresql://... - ANALYTICS_DB_AUTOLOAD=true - ANALYTICS_DB_SYNC=false - ANALYTICS_DATA_RETENTION_DAYS=90 - RESPECT_DNT_HEADER=true - ``` - -### Step 4: Restart Application -- [ ] Restart backend service -- [ ] Check startup logs for: - - "Analytics database connection initialized" - - No TypeORM errors - - All modules loaded successfully - ---- - -## Post-Deployment Verification - -### Basic Functionality Tests - -#### 1. Check Tracking is Working -```bash -# Make a test request -curl http://your-api.com/api/puzzles - -# Wait 5 seconds, then check activities -curl http://your-api.com/analytics/activities?limit=5 -``` -Expected: Should see recent activity records - -#### 2. Test Metrics API -```bash -# Get DAU -curl http://your-api.com/analytics/metrics/dau - -# Get session duration -curl http://your-api.com/analytics/metrics/session-duration -``` -Expected: Should return JSON with metrics - -#### 3. Verify Swagger Docs -``` -Visit: http://your-api.com/docs -Search for: "Analytics" section -``` -Expected: 9 analytics endpoints documented - -### Performance Checks - -#### Response Time Impact -- [ ] Measure average request latency (should be <2ms increase) -- [ ] Check p95 latency (should be <10ms increase) -- [ ] Monitor database query times - -#### Database Performance -- [ ] Check analytics DB CPU usage -- [ ] Monitor connection pool -- [ ] Verify indexes are being used - -### Privacy Compliance Checks - -#### Data Anonymization -- [ ] Query recent activities: - ```sql - SELECT "anonymizedIp" FROM user_activities LIMIT 10; - ``` - Expected: IPs should end with 'xxx' (e.g., 192.168.1.xxx) - -#### Metadata Sanitization -- [ ] Check metadata doesn't contain PII: - ```sql - SELECT metadata FROM user_activities WHERE metadata IS NOT NULL LIMIT 5; - ``` - Expected: No email, password, phone fields - -#### Opt-Out Mechanism -- [ ] Test opt-out functionality (if API endpoint implemented) -- [ ] Verify DNT header respected when set - ---- - -## Monitoring Setup - -### Logs to Monitor - -#### Application Logs -Watch for these log messages: -- ✅ "Analytics database connection initialized" -- ✅ "Daily metrics calculated for {date}" -- âš ī¸ "Activity tracking error: {message}" -- âš ī¸ "Failed to record activity: {message}" -- â„šī¸ "Deleted {count} expired activities" - -#### Database Logs -Monitor: -- Connection count -- Query execution times -- Deadlock detection -- Disk usage growth - -### Alerts to Configure - -#### Critical Alerts -- [ ] Analytics DB connection failures -- [ ] Activity write failure rate > 5% -- [ ] Daily cleanup job failures -- [ ] Response latency increase > 50ms - -#### Warning Alerts -- [ ] High database CPU (>80%) -- [ ] Low disk space on analytics DB -- [ ] Cache miss rate > 20% -- [ ] Unusual traffic spikes - -### Dashboards to Build - -#### Real-time Dashboard -Metrics to display: -- Active users (last 5 min) -- Requests per second -- Average response time -- Error rate - -#### Daily Analytics Dashboard -Metrics to display: -- DAU trend (7-day view) -- WAU trend (4-week view) -- Average session duration -- Top features by usage -- Platform distribution -- Device distribution - ---- - -## Rollback Plan - -### If Issues Occur - -#### Option 1: Disable Tracking Temporarily -```bash -# In .env file -ANALYTICS_DB_AUTOLOAD=false -``` -Then restart service. - -#### Option 2: Reduce Logging Volume -```bash -# Track only critical events -# Modify middleware to filter by event type -``` - -#### Option 3: Full Rollback -1. Revert code to previous version -2. Keep analytics DB (data will be preserved) -3. Resume normal operations - -### Data Preservation -- Analytics data is retained even if disabled -- Can re-enable at any time -- Historical data remains queryable - ---- - -## Success Criteria - -### Week 1 Metrics -- [ ] Zero tracking-related errors -- [ ] <2ms average latency impact -- [ ] All 9 API endpoints responding -- [ ] Daily cleanup job runs successfully -- [ ] Metrics calculation completes without errors - -### Month 1 Metrics -- [ ] 99.9% tracking accuracy -- [ ] <1% write failure rate -- [ ] Positive team feedback -- [ ] Privacy compliance verified -- [ ] Dashboard built and in use - ---- - -## Team Communication - -### Notify Stakeholders - -#### Product Team -Subject: Analytics Tracking Now Available - -"We've implemented comprehensive user activity tracking with privacy-compliant analytics. You can now access: -- Daily/Weekly active users -- Feature usage statistics -- Session duration metrics -- Platform/device breakdowns - -API docs: http://your-api.com/docs" - -#### Engineering Team -Subject: New Analytics Middleware Deployed - -"The analytics middleware is now live. Key points: -- Automatic tracking (no code changes needed) -- <2ms performance impact -- GDPR/CCPA compliant -- 9 new REST endpoints -- Full documentation in backend/src/analytics/ - -Questions? Check QUICKSTART.md or README.md" - -#### Legal/Compliance Team -Subject: Privacy-Compliant Analytics Implemented - -"We've deployed a new analytics system with: -- IP anonymization -- No PII storage -- Do-Not-Track support -- 90-day auto-deletion -- Opt-out capability - -Ready for compliance review." - ---- - -## Optional Enhancements (Future) - -### Phase 2 Features -- [ ] Real-time WebSocket dashboard -- [ ] Custom event tracking API -- [ ] User segmentation -- [ ] Funnel analysis -- [ ] Retention cohorts -- [ ] A/B testing support -- [ ] Export functionality (CSV/PDF) -- [ ] Scheduled email reports - -### Advanced Monitoring -- [ ] Anomaly detection -- [ ] Predictive analytics -- [ ] User journey mapping -- [ ] Conversion tracking - ---- - -## Support Contacts - -### Technical Issues -- Review: `backend/src/analytics/README.md` -- Quick reference: `backend/src/analytics/QUICKSTART.md` -- Implementation details: Check source code comments - -### Escalation Path -1. Check logs and documentation -2. Test in staging environment -3. Consult team chat/channel -4. Create GitHub issue with details - ---- - -**Deployment Date**: _______________ -**Deployed By**: _______________ -**Version**: 1.0.0 -**Status**: ☐ Pending ☐ In Progress ☐ Complete ☐ Rolled Back - ---- - -## Sign-Off - -- [ ] Engineering Lead Approval -- [ ] Product Owner Notification -- [ ] Compliance Team Review (if required) -- [ ] Monitoring Dashboards Configured -- [ ] On-Call Team Briefed - -**Ready for Production**: ☐ Yes ☐ No -**Date Approved**: _______________ diff --git a/backend/scripts/create-analytics-tables.sql b/backend/scripts/create-analytics-tables.sql deleted file mode 100644 index 1ff338f..0000000 --- a/backend/scripts/create-analytics-tables.sql +++ /dev/null @@ -1,124 +0,0 @@ --- Migration: Create Analytics Tables --- Date: 2026-03-26 --- Description: Creates tables for user activity tracking system - --- Enable UUID extension -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- Create enum types -CREATE TYPE event_type_enum AS ENUM ( - 'authentication', - 'puzzle', - 'quest', - 'profile', - 'social', - 'achievement', - 'category', - 'other' -); - -CREATE TYPE event_category_enum AS ENUM ( - 'login', 'logout', 'signup', 'password_reset_request', 'password_reset_complete', - 'puzzle_started', 'puzzle_submitted', 'puzzle_completed', 'puzzle_hint_viewed', 'puzzle_skipped', - 'daily_quest_viewed', 'daily_quest_progress_updated', 'daily_quest_completed', 'daily_quest_claimed', - 'category_viewed', 'category_filtered', 'puzzle_list_viewed', - 'profile_updated', 'profile_picture_uploaded', 'preferences_updated', 'privacy_settings_changed', - 'friend_request_sent', 'friend_request_accepted', 'challenge_sent', 'challenge_accepted', 'challenge_completed', - 'achievement_unlocked', 'points_earned', 'points_redeemed', 'streak_milestone_reached', - 'page_view', 'api_call', 'error' -); - -CREATE TYPE device_type_enum AS ENUM ('desktop', 'mobile', 'tablet', 'unknown'); - -CREATE TYPE platform_type_enum AS ENUM ('web', 'mobile_web', 'pwa', 'api'); - -CREATE TYPE consent_status_enum AS ENUM ('opted-in', 'opted-out', 'not-set'); - --- User Activities Table -CREATE TABLE IF NOT EXISTS user_activities ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - "userId" UUID, - "sessionId" UUID NOT NULL, - "eventType" event_type_enum NOT NULL, - "eventCategory" event_category_enum NOT NULL, - "timestamp" TIMESTAMPTZ NOT NULL DEFAULT NOW(), - "duration" BIGINT DEFAULT 0, - "metadata" JSONB, - "browser" VARCHAR(100), - "os" VARCHAR(100), - "deviceType" device_type_enum DEFAULT 'unknown', - "platform" platform_type_enum DEFAULT 'web', - "country" VARCHAR(2), - "city" VARCHAR(100), - "anonymizedIp" VARCHAR(45), - "userAgent" TEXT, - "referrer" TEXT, - "isAnonymous" BOOLEAN DEFAULT FALSE, - "consentStatus" consent_status_enum DEFAULT 'not-set', - "dataRetentionExpiry" TIMESTAMPTZ, - "createdAt" TIMESTAMPTZ DEFAULT NOW() -); - --- Analytics Sessions Table -CREATE TABLE IF NOT EXISTS analytics_sessions ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - "userId" UUID, - "sessionId" UUID UNIQUE NOT NULL, - "anonymizedIp" VARCHAR(45), - "userAgent" TEXT, - "browser" VARCHAR(100), - "os" VARCHAR(100), - "deviceType" VARCHAR(20) DEFAULT 'unknown', - "platform" VARCHAR(20) DEFAULT 'web', - "country" VARCHAR(2), - "city" VARCHAR(100), - "startedAt" TIMESTAMPTZ DEFAULT NOW(), - "lastActivityAt" TIMESTAMPTZ, - "totalDuration" BIGINT DEFAULT 0, - "activityCount" INTEGER DEFAULT 0, - "isAnonymous" BOOLEAN DEFAULT FALSE, - "consentStatus" VARCHAR(20) DEFAULT 'not-set', - "updatedAt" TIMESTAMPTZ DEFAULT NOW(), - "createdAt" TIMESTAMPTZ DEFAULT NOW() -); - --- Analytics Metrics Table -CREATE TABLE IF NOT EXISTS analytics_metrics ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - "date" DATE NOT NULL, - "metricType" VARCHAR(50) NOT NULL, - "value" JSONB NOT NULL, - "period" VARCHAR(10), - "count" INTEGER DEFAULT 0, - "sum" BIGINT DEFAULT 0, - "breakdown" JSONB, - "updatedAt" TIMESTAMPTZ DEFAULT NOW(), - "createdAt" TIMESTAMPTZ DEFAULT NOW() -); - --- Create indexes for performance -CREATE INDEX IF NOT EXISTS idx_user_activities_session_id ON user_activities("sessionId"); -CREATE INDEX IF NOT EXISTS idx_user_activities_user_id ON user_activities("userId"); -CREATE INDEX IF NOT EXISTS idx_user_activities_event_type ON user_activities("eventType", "eventCategory"); -CREATE INDEX IF NOT EXISTS idx_user_activities_timestamp ON user_activities("timestamp"); -CREATE INDEX IF NOT EXISTS idx_user_activities_retention ON user_activities("dataRetentionExpiry"); - -CREATE INDEX IF NOT EXISTS idx_analytics_sessions_user_id ON analytics_sessions("userId"); -CREATE INDEX IF NOT EXISTS idx_analytics_sessions_session_id ON analytics_sessions("sessionId"); -CREATE INDEX IF NOT EXISTS idx_analytics_sessions_last_activity ON analytics_sessions("lastActivityAt"); - -CREATE INDEX IF NOT EXISTS idx_analytics_metrics_date ON analytics_metrics("date"); -CREATE INDEX IF NOT EXISTS idx_analytics_metrics_type ON analytics_metrics("metricType"); - --- Add comments for documentation -COMMENT ON TABLE user_activities IS 'Stores individual user activity events for analytics'; -COMMENT ON TABLE analytics_sessions IS 'Tracks user sessions with aggregated metrics'; -COMMENT ON TABLE analytics_metrics IS 'Aggregated daily metrics for reporting'; - -COMMENT ON COLUMN user_activities."anonymizedIp" IS 'IP address with last octet removed for privacy'; -COMMENT ON COLUMN user_activities."metadata" IS 'Sanitized JSONB - no PII'; -COMMENT ON COLUMN user_activities."dataRetentionExpiry" IS 'Auto-delete after this date (90 days)'; - --- Grant permissions (adjust as needed) --- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO analytics_user; --- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO analytics_user; diff --git a/backend/src/analytics/QUICKSTART.md b/backend/src/analytics/QUICKSTART.md deleted file mode 100644 index 0debca1..0000000 --- a/backend/src/analytics/QUICKSTART.md +++ /dev/null @@ -1,184 +0,0 @@ -# Analytics Quick Start Guide - -## Setup (5 minutes) - -### 1. Add Environment Variables - -Copy these to your `.env` file: - -```bash -# Quick setup - uses same DB as main app -ANALYTICS_DB_AUTOLOAD=true -ANALYTICS_DB_SYNC=true -ANALYTICS_DATA_RETENTION_DAYS=90 -RESPECT_DNT_HEADER=true -``` - -### 2. Install Dependencies (if needed) - -```bash -npm install @nestjs/schedule -``` - -### 3. Run Database Sync - -```bash -npm run start:dev -# TypeORM will auto-create tables on first run -``` - -That's it! Analytics is now tracking all user activity automatically. - ---- - -## Viewing Analytics Data - -### Test It Out - -1. Make some requests to your API -2. Query the analytics: - -```bash -# Get recent activities -curl http://localhost:3000/analytics/activities?limit=10 - -# Get today's DAU -curl http://localhost:3000/analytics/metrics/dau - -# Get feature usage -curl "http://localhost:3000/analytics/metrics/feature-usage?startDate=$(date -d '7 days ago' +%Y-%m-%d)&endDate=$(date +%Y-%m-%d)" -``` - -### Swagger UI - -Visit `http://localhost:3000/docs` and look for the **Analytics** section. - ---- - -## Common Tasks - -### Check if Tracking is Working - -```typescript -// In any service, inject and query: -import { ActivityService } from './analytics/providers/activity.service'; - -constructor(private activityService: ActivityService) {} - -async checkTracking() { - const recent = await this.activityService.getRecentActivities({ limit: 5 }); - console.log('Recent activities:', recent); -} -``` - -### Manually Track an Event - -```typescript -await this.activityService.recordActivity({ - userId: 'user-123', - sessionId: 'session-456', - eventType: 'other', - eventCategory: 'custom_action', - duration: 50, - metadata: { action: 'button_clicked' }, - isAnonymous: false, - consentStatus: 'opted-in', -}); -``` - -### Check User Opt-Out Status - -```typescript -const isOptedOut = await this.privacyService.isOptedOut('user-id'); -if (!isOptedOut) { - // Track activity -} -``` - ---- - -## Troubleshooting - -### No Activities Showing Up? - -1. Check logs for "Analytics database connection initialized" -2. Verify `.env` has `ANALYTICS_DB_AUTOLOAD=true` -3. Check database tables were created: - ```sql - \dt public.*analytics* - ``` - -### Getting Errors? - -1. Check backend logs: `npm run start:dev` -2. Look for "Activity tracking error" messages -3. Ensure all environment variables are set - -### Want to Disable Temporarily? - -Set in `.env`: -```bash -ANALYTICS_DB_AUTOLOAD=false -``` - -Restart server. No code changes needed. - ---- - -## Performance Tips - -### For Production - -1. **Use separate database:** - ```bash - ANALYTICS_DB_URL=postgresql://user:pass@host:5432/analytics_db - ``` - -2. **Enable Redis caching** (already configured) - -3. **Tune retention:** - ```bash - ANALYTICS_DATA_RETENTION_DAYS=30 # Shorter period - ``` - -### Monitor These Metrics - -- Request latency (should be <2ms impact) -- Database write failures -- Cache hit rate - ---- - -## Privacy Compliance - -### User Requests Data Deletion - -```typescript -// Delete all activities for a user -await this.activityService.deleteUserActivities('user-id'); -``` - -### User Wants to Opt Out - -```typescript -await this.privacyService.setOptOut('user-id', true); -``` - -### Export User Data - -```typescript -const activities = await this.activityService.getUserActivities('user-id', 1000); -``` - ---- - -## Next Steps - -1. **Review Full Documentation**: See `README.md` in analytics folder -2. **Add Custom Events**: Track domain-specific actions -3. **Build Dashboard**: Use analytics API endpoints -4. **Set Up Alerts**: Monitor failed writes, high latency - ---- - -**Questions?** Check the full README or ask the team! diff --git a/backend/src/analytics/README.md b/backend/src/analytics/README.md deleted file mode 100644 index 6ef643e..0000000 --- a/backend/src/analytics/README.md +++ /dev/null @@ -1,464 +0,0 @@ -# User Activity Tracking Middleware - Implementation Guide - -## Overview - -This implementation provides a comprehensive, privacy-compliant user activity tracking system for the MindBlock backend. It automatically captures user interactions, stores them asynchronously in a separate analytics database, and provides queryable endpoints for engagement metrics. - -## Features Implemented - -✅ **Automatic Activity Tracking** - All significant user actions tracked automatically -✅ **Async Processing** - No request delay (<2ms impact) -✅ **Privacy Compliance** - GDPR/CCPA compliant with opt-out support -✅ **Anonymous Tracking** - Session-based tracking for anonymous users -✅ **Analytics API** - Queryable REST endpoints for metrics -✅ **Data Retention** - Automatic 90-day data cleanup -✅ **Real-time Metrics** - DAU, WAU, session duration, feature usage - ---- - -## Architecture - -### Components Created - -``` -backend/src/analytics/ -├── entities/ -│ ├── user-activity.entity.ts # Main activity log -│ ├── session.entity.ts # Session tracking -│ └── metrics.entity.ts # Aggregated metrics -├── providers/ -│ ├── analytics-db.service.ts # DB connection manager -│ ├── activity.service.ts # Activity CRUD operations -│ ├── metrics.service.ts # Metrics calculation -│ ├── privacy-preferences.service.ts # Opt-out management -│ └── data-retention.service.ts # Automated cleanup jobs -├── middleware/ -│ └── activity-tracker.middleware.ts # Core tracking middleware -├── utils/ -│ └── data-anonymizer.ts # PII removal utilities -├── controllers/ -│ └── analytics.controller.ts # REST API endpoints -└── analytics.module.ts # Module configuration -``` - ---- - -## Configuration - -### Environment Variables - -Add to `.env`: - -```bash -# Analytics Database (Optional - falls back to main DB) -ANALYTICS_DB_URL=postgresql://analytics_user:password@localhost:5432/mindblock_analytics -ANALYTICS_DB_HOST=localhost -ANALYTICS_DB_PORT=5433 -ANALYTICS_DB_USER=analytics_user -ANALYTICS_DB_PASSWORD=secure_password -ANALYTICS_DB_NAME=mindblock_analytics -ANALYTICS_DB_SYNC=false -ANALYTICS_DB_AUTOLOAD=true - -# Data Retention -ANALYTICS_DATA_RETENTION_DAYS=90 - -# Privacy Defaults -TRACKING_OPT_OUT_BY_DEFAULT=false -RESPECT_DNT_HEADER=true -``` - -### Database Setup - -If using a separate analytics database: - -```sql -CREATE DATABASE mindblock_analytics; -CREATE USER analytics_user WITH PASSWORD 'secure_password'; -GRANT ALL PRIVILEGES ON DATABASE mindblock_analytics TO analytics_user; -``` - ---- - -## Usage - -### Automatic Tracking - -The middleware automatically tracks: - -1. **Authentication Events** - - Login, logout, signup - - Password reset requests - -2. **Puzzle Interactions** - - Puzzle started, submitted, completed - - Hints viewed, puzzles skipped - -3. **Quest Progress** - - Daily quests viewed, progressed, completed, claimed - -4. **Category Browsing** - - Categories viewed, filtered - -5. **Profile Updates** - - Profile changes, avatar uploads, preferences - -6. **Social Interactions** - - Friend requests, challenges - -7. **Achievements** - - Unlocks, points earned/redeemed, streak milestones - -### Manual Tracking (Optional) - -Inject `ActivityService` to manually track custom events: - -```typescript -import { ActivityService } from './analytics/providers/activity.service'; - -constructor(private activityService: ActivityService) {} - -async trackCustomEvent(userId: string, sessionId: string) { - await this.activityService.recordActivity({ - userId, - sessionId, - eventType: 'other', - eventCategory: 'custom_event', - duration: 100, - metadata: { customField: 'value' }, - isAnonymous: false, - consentStatus: 'opted-in', - }); -} -``` - ---- - -## API Endpoints - -### Get Daily Active Users -```http -GET /analytics/metrics/dau?date=2024-01-15 -``` - -### Get Weekly Active Users -```http -GET /analytics/metrics/wau?date=2024-01-15 -``` - -### Get Average Session Duration -```http -GET /analytics/metrics/session-duration?date=2024-01-15 -``` - -### Get Feature Usage Statistics -```http -GET /analytics/metrics/feature-usage?startDate=2024-01-01&endDate=2024-01-31 -``` - -### Get Platform Distribution -```http -GET /analytics/metrics/platform-distribution?startDate=2024-01-01&endDate=2024-01-31 -``` - -### Get Device Distribution -```http -GET /analytics/metrics/device-distribution?startDate=2024-01-01&endDate=2024-01-31 -``` - -### Get Recent Activities -```http -GET /analytics/activities?limit=100&offset=0 -``` - -### Get User-Specific Activities -```http -GET /analytics/activities/:userId?limit=100 -``` - -### Query Activities with Filters -```http -POST /analytics/activities/query -Content-Type: application/json - -{ - "eventType": "puzzle", - "eventCategory": "puzzle_completed", - "startDate": "2024-01-01", - "endDate": "2024-01-31", - "limit": 50 -} -``` - ---- - -## Privacy Compliance - -### Features - -1. **IP Anonymization** - - Last octet removed for IPv4 (192.168.1.xxx) - - Interface ID removed for IPv6 - -2. **Do-Not-Track Support** - - Respects DNT header when enabled - - Configurable via `RESPECT_DNT_HEADER` env var - -3. **Opt-Out Mechanism** - - Redis-backed opt-out status - - Users can toggle tracking preference - - Cached for 1 hour - -4. **Data Retention** - - Automatic deletion after 90 days - - Daily cleanup job at 2 AM UTC - - Configurable via `ANALYTICS_DATA_RETENTION_DAYS` - -5. **PII Protection** - - Email, password, phone fields filtered - - Metadata sanitization - - Country/city level only (no coordinates) - -### Opt-Out API (Future Enhancement) - -```typescript -// Example endpoint to implement -@Post('analytics/opt-out') -async optOut(@Body() body: { userId: string; optOut: boolean }) { - await this.privacyService.setOptOut(body.userId, body.optOut); -} -``` - ---- - -## Data Structure - -### Activity Record - -```typescript -{ - id: 'uuid', - userId?: 'uuid', // Optional for anonymous - sessionId: 'uuid', // Required for all - eventType: 'authentication' | 'puzzle' | 'quest' | ..., - eventCategory: 'login' | 'puzzle_solved' | ..., - timestamp: Date, - duration: number, // milliseconds - metadata: { // Sanitized JSONB - path: '/puzzles/123', - method: 'POST', - statusCode: 200, - }, - browser: 'Chrome', - os: 'Windows 11', - deviceType: 'desktop', - platform: 'web', - country: 'US', - city: 'New York', - anonymizedIp: '192.168.1.xxx', - userAgent: 'Mozilla/5.0...', - referrer: 'https://google.com', - isAnonymous: boolean, - consentStatus: 'opted-in' | 'opted-out' | 'not-set', - dataRetentionExpiry: Date, // auto-calculated -} -``` - ---- - -## Performance - -### Optimizations - -1. **Async Processing** - - Activity recording happens after response sent - - Non-blocking database writes - - Response time impact: <2ms average - -2. **Caching** - - Opt-out status cached in Redis (1 hour) - - GeoIP data cached (24 hours) - -3. **Batch Operations** - - Future enhancement: batch inserts every 100 events - - Scheduled metrics calculation (daily at 2 AM) - -4. **Database Separation** - - Separate analytics DB prevents contention - - Falls back to main DB if not configured - -### Benchmarks - -To run performance benchmarks: - -```bash -# Add benchmark script to package.json -npm run benchmark:analytics -``` - -Expected results: -- Middleware overhead: <2ms -- Async write latency: 10-50ms (non-blocking) -- Cache hit rate: >90% - ---- - -## Monitoring & Maintenance - -### Daily Jobs - -1. **Data Cleanup** (2 AM UTC) - - Deletes activities older than 90 days - - Logs deletion count - -2. **Metrics Calculation** (2 AM UTC) - - Calculates DAU, WAU for previous day - - Computes averages and distributions - - Saves aggregated metrics - -### Logging - -All analytics operations are logged with appropriate levels: -- `log` - Successful operations -- `error` - Failures (non-blocking) -- `warn` - Configuration issues - -### Health Checks - -Monitor these indicators: -- Analytics DB connection status -- Daily job execution success -- Activity write failure rate -- Cache hit rate - ---- - -## Migration Strategy - -### Phase 1: Deployment (Week 1) -1. Deploy analytics database schema -2. Enable middleware in "shadow mode" (log only) -3. Monitor performance impact - -### Phase 2: Gradual Rollout (Week 2-3) -1. Enable full tracking for internal users -2. Verify data accuracy -3. Test API endpoints - -### Phase 3: Full Enablement (Week 4) -1. Enable for all users -2. Monitor dashboard metrics -3. Collect feedback - -### Phase 4: Optimization (Ongoing) -1. Analyze performance data -2. Tune retention policies -3. Add advanced features (real-time streaming) - ---- - -## Troubleshooting - -### Issue: Analytics not tracking - -**Solution:** -1. Check `ANALYTICS_DB_*` environment variables -2. Verify database connection -3. Check logs for errors -4. Ensure `AnalyticsModule` is imported in `app.module.ts` - -### Issue: High latency - -**Solution:** -1. Check analytics DB performance -2. Verify Redis cache is working -3. Review async processing queue -4. Consider separate DB instance - -### Issue: Data not appearing in API - -**Solution:** -1. Check entity migrations ran -2. Verify TypeORM synchronization -3. Check query date ranges -4. Review database permissions - ---- - -## Future Enhancements - -### Real-time Dashboard (WebSocket) -```typescript -@WebSocketGateway() -export class AnalyticsGateway { - @SubscribeMessage('getActiveUsers') - handleActiveUsers(client: Socket) { - // Emit active user count every 30s - } -} -``` - -### Advanced Segmentation -- User cohorts based on behavior -- Funnel analysis -- Retention curves - -### Export Functionality -- CSV/JSON export -- Scheduled reports -- Integration with BI tools - -### A/B Testing Support -- Experiment tracking -- Conversion metrics -- Statistical significance - ---- - -## Security Considerations - -1. **Access Control** - - Analytics endpoints should be admin-only - - Implement role-based access control - - Rate limit queries - -2. **Data Encryption** - - Encrypt analytics DB at rest - - Use TLS for connections - - Hash session IDs - -3. **Audit Logging** - - Log all analytics API access - - Track who queried what data - - Retain audit logs separately - ---- - -## Compliance Checklist - -- ✅ IP addresses anonymized -- ✅ No PII stored in metadata -- ✅ Do-Not-Track header supported -- ✅ User opt-out mechanism implemented -- ✅ Data retention policy enforced (90 days) -- ✅ Country/city level only (no precise location) -- ✅ Session-based anonymous tracking -- ✅ Consent status logged with each event -- ✅ Separate analytics database -- ✅ Automated cleanup jobs - ---- - -## Support - -For issues or questions: -1. Check implementation guide above -2. Review code comments in source files -3. Check backend logs for errors -4. Consult team documentation - ---- - -**Implementation Date:** March 2026 -**Version:** 1.0.0 -**Status:** Production Ready diff --git a/backend/src/analytics/analytics.module.ts b/backend/src/analytics/analytics.module.ts deleted file mode 100644 index 7c1cd05..0000000 --- a/backend/src/analytics/analytics.module.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { AnalyticsDbService } from './providers/analytics-db.service'; -import { ActivityService } from './providers/activity.service'; -import { MetricsService } from './providers/metrics.service'; -import { PrivacyPreferencesService } from './providers/privacy-preferences.service'; -import { DataRetentionService } from './providers/data-retention.service'; -import { DataAnonymizer } from './utils/data-anonymizer'; -import { AnalyticsController } from './controllers/analytics.controller'; -import { UserActivity } from './entities/user-activity.entity'; -import { AnalyticsSession } from './entities/session.entity'; -import { AnalyticsMetric } from './entities/metrics.entity'; - -@Global() -@Module({ - imports: [ - TypeOrmModule.forFeature([ - UserActivity, - AnalyticsSession, - AnalyticsMetric, - ]), - ], - providers: [ - AnalyticsDbService, - ActivityService, - MetricsService, - PrivacyPreferencesService, - DataRetentionService, - DataAnonymizer, - AnalyticsController, - ], - exports: [ - AnalyticsDbService, - ActivityService, - MetricsService, - PrivacyPreferencesService, - DataRetentionService, - DataAnonymizer, - AnalyticsController, - TypeOrmModule, - ], -}) -export class AnalyticsModule {} diff --git a/backend/src/analytics/controllers/analytics.controller.ts b/backend/src/analytics/controllers/analytics.controller.ts deleted file mode 100644 index f373a46..0000000 --- a/backend/src/analytics/controllers/analytics.controller.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { ApiTags, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { Controller, Get, Query, Param, Post, Body, UseGuards } from '@nestjs/common'; -import { MetricsService } from '../providers/metrics.service'; -import { ActivityService } from '../providers/activity.service'; -import { AnalyticsMetric, UserActivity } from '../entities'; - -@ApiTags('Analytics') -@Controller('analytics') -export class AnalyticsController { - constructor( - private readonly metricsService: MetricsService, - private readonly activityService: ActivityService, - ) {} - - @Get('metrics/dau') - @ApiOperation({ summary: 'Get Daily Active Users' }) - @ApiQuery({ name: 'date', required: false, description: 'Date (YYYY-MM-DD)' }) - @ApiResponse({ status: 200, description: 'Returns DAU count' }) - async getDau(@Query('date') date?: string): Promise<{ count: number; date: string }> { - const targetDate = date ? new Date(date) : new Date(); - const count = await this.metricsService.calculateDau(targetDate); - return { - count, - date: this.formatDate(targetDate), - }; - } - - @Get('metrics/wau') - @ApiOperation({ summary: 'Get Weekly Active Users' }) - @ApiQuery({ name: 'date', required: false, description: 'Date (YYYY-MM-DD)' }) - @ApiResponse({ status: 200, description: 'Returns WAU count' }) - async getWau(@Query('date') date?: string): Promise<{ count: number; week: string }> { - const targetDate = date ? new Date(date) : new Date(); - const count = await this.metricsService.calculateWau(targetDate); - return { - count, - week: this.getWeekNumber(targetDate).toString(), - }; - } - - @Get('metrics/session-duration') - @ApiOperation({ summary: 'Get average session duration' }) - @ApiQuery({ name: 'date', required: false, description: 'Date (YYYY-MM-DD)' }) - @ApiResponse({ status: 200, description: 'Returns average session duration in ms' }) - async getSessionDuration(@Query('date') date?: string): Promise<{ average: number; unit: string }> { - const targetDate = date ? new Date(date) : new Date(); - const average = await this.metricsService.calculateAverageSessionDuration(targetDate); - return { - average, - unit: 'milliseconds', - }; - } - - @Get('metrics/feature-usage') - @ApiOperation({ summary: 'Get feature usage statistics' }) - @ApiQuery({ name: 'startDate', required: true }) - @ApiQuery({ name: 'endDate', required: true }) - @ApiResponse({ status: 200, description: 'Returns feature usage breakdown' }) - async getFeatureUsage( - @Query('startDate') startDate: string, - @Query('endDate') endDate: string, - ): Promise> { - return await this.metricsService.getFeatureUsageStatistics( - new Date(startDate), - new Date(endDate), - ); - } - - @Get('metrics/platform-distribution') - @ApiOperation({ summary: 'Get platform distribution' }) - @ApiQuery({ name: 'startDate', required: true }) - @ApiQuery({ name: 'endDate', required: true }) - @ApiResponse({ status: 200, description: 'Returns platform breakdown' }) - async getPlatformDistribution( - @Query('startDate') startDate: string, - @Query('endDate') endDate: string, - ): Promise> { - return await this.metricsService.getPlatformDistribution( - new Date(startDate), - new Date(endDate), - ); - } - - @Get('metrics/device-distribution') - @ApiOperation({ summary: 'Get device distribution' }) - @ApiQuery({ name: 'startDate', required: true }) - @ApiQuery({ name: 'endDate', required: true }) - @ApiResponse({ status: 200, description: 'Returns device breakdown' }) - async getDeviceDistribution( - @Query('startDate') startDate: string, - @Query('endDate') endDate: string, - ): Promise> { - return await this.metricsService.getDeviceDistribution( - new Date(startDate), - new Date(endDate), - ); - } - - @Get('activities') - @ApiOperation({ summary: 'Get recent activities' }) - @ApiQuery({ name: 'limit', required: false, default: 100 }) - @ApiQuery({ name: 'offset', required: false, default: 0 }) - @ApiResponse({ status: 200, description: 'Returns activity logs' }) - async getActivities( - @Query('limit') limit: number = 100, - @Query('offset') offset: number = 0, - ): Promise { - return await this.activityService.getRecentActivities({ - limit, - }); - } - - @Get('activities/:userId') - @ApiOperation({ summary: 'Get user-specific activities' }) - @ApiQuery({ name: 'limit', required: false, default: 100 }) - @ApiResponse({ status: 200, description: 'Returns user activities' }) - async getUserActivities( - @Param('userId') userId: string, - @Query('limit') limit: number = 100, - ): Promise { - return await this.activityService.getUserActivities(userId, limit); - } - - @Post('activities/query') - @ApiOperation({ summary: 'Query activities with filters' }) - @ApiResponse({ status: 200, description: 'Returns filtered activities' }) - async queryActivities( - @Body() filters: { - eventType?: string; - eventCategory?: string; - startDate?: string; - endDate?: string; - limit?: number; - }, - ): Promise { - return await this.activityService.getRecentActivities({ - eventType: filters.eventType as any, - eventCategory: filters.eventCategory as any, - startDate: filters.startDate ? new Date(filters.startDate) : undefined, - endDate: filters.endDate ? new Date(filters.endDate) : undefined, - limit: filters.limit || 100, - }); - } - - private formatDate(date: Date): string { - return date.toISOString().split('T')[0]; - } - - private getWeekNumber(d: Date): number { - const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())); - const dayNum = date.getUTCDay() || 7; - date.setUTCDate(date.getUTCDate() + 4 - dayNum); - const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); - return Math.ceil((((date.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); - } -} diff --git a/backend/src/analytics/entities/index.ts b/backend/src/analytics/entities/index.ts deleted file mode 100644 index 89083d2..0000000 --- a/backend/src/analytics/entities/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './user-activity.entity'; -export * from './session.entity'; -export * from './metrics.entity'; diff --git a/backend/src/analytics/entities/metrics.entity.ts b/backend/src/analytics/entities/metrics.entity.ts deleted file mode 100644 index a1f5a1e..0000000 --- a/backend/src/analytics/entities/metrics.entity.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - CreateDateColumn, - UpdateDateColumn, - Index, -} from 'typeorm'; - -@Entity('analytics_metrics') -@Index(['date']) -@Index(['metricType']) -export class AnalyticsMetric { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column('date') - @Index() - date: string; // YYYY-MM-DD format - - @Column({ - type: 'enum', - enum: [ - 'dau', // Daily Active Users - 'wau', // Weekly Active Users - 'mau', // Monthly Active Users - 'session_duration_avg', - 'session_duration_median', - 'total_sessions', - 'total_activities', - 'feature_usage', - 'event_type_distribution', - 'platform_distribution', - 'device_distribution', - 'geographic_distribution', - 'retention_rate', - 'churn_rate', - ], - }) - metricType: string; - - @Column('jsonb') - value: Record; - - @Column('varchar', { length: 10, nullable: true }) - period?: string; // For weekly/monthly aggregations: '2024-W01', '2024-01' - - @Column('integer', { default: 0 }) - count: number; - - @Column('bigint', { default: 0 }) - sum?: number; // For aggregatable metrics - - @Column('jsonb', { nullable: true }) - breakdown?: Record; // Detailed breakdown by category/type - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; -} diff --git a/backend/src/analytics/entities/session.entity.ts b/backend/src/analytics/entities/session.entity.ts deleted file mode 100644 index 631fc9b..0000000 --- a/backend/src/analytics/entities/session.entity.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - CreateDateColumn, - UpdateDateColumn, - Index, - ManyToOne, - JoinColumn, - OneToMany, -} from 'typeorm'; -import { UserActivity } from './user-activity.entity'; - -@Entity('analytics_sessions') -@Index(['userId']) -@Index(['sessionId']) -@Index(['lastActivityAt']) -export class AnalyticsSession { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column('uuid', { nullable: true }) - @Index() - userId?: string; - - @Column('uuid', { unique: true }) - sessionId: string; - - @Column('varchar', { length: 45, nullable: true }) - anonymizedIp?: string; - - @Column('text', { nullable: true }) - userAgent?: string; - - @Column('varchar', { length: 100, nullable: true }) - browser?: string; - - @Column('varchar', { length: 100, nullable: true }) - os?: string; - - @Column({ - type: 'enum', - enum: ['desktop', 'mobile', 'tablet', 'unknown'], - default: 'unknown', - }) - deviceType: string; - - @Column({ - type: 'enum', - enum: ['web', 'mobile_web', 'pwa', 'api'], - default: 'web', - }) - platform: string; - - @Column('varchar', { length: 2, nullable: true }) - country?: string; - - @Column('varchar', { length: 100, nullable: true }) - city?: string; - - @CreateDateColumn({ name: 'started_at', type: 'timestamptz' }) - startedAt: Date; - - @Column('timestamptz', { nullable: true }) - @Index() - lastActivityAt?: Date; - - @Column('bigint', { default: 0 }) - totalDuration: number; // in milliseconds - - @Column('integer', { default: 0 }) - activityCount: number; - - @Column({ default: false }) - isAnonymous: boolean; - - @Column({ - type: 'enum', - enum: ['opted-in', 'opted-out', 'not-set'], - default: 'not-set', - }) - consentStatus: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relationships - @OneToMany(() => UserActivity, (activity) => activity.sessionId) - activities: UserActivity[]; -} diff --git a/backend/src/analytics/entities/user-activity.entity.ts b/backend/src/analytics/entities/user-activity.entity.ts deleted file mode 100644 index 29058de..0000000 --- a/backend/src/analytics/entities/user-activity.entity.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - CreateDateColumn, - Index, -} from 'typeorm'; - -export type EventType = - | 'authentication' - | 'puzzle' - | 'quest' - | 'profile' - | 'social' - | 'achievement' - | 'category' - | 'other'; - -export type EventCategory = - // Authentication - | 'login' - | 'logout' - | 'signup' - | 'password_reset_request' - | 'password_reset_complete' - // Puzzle - | 'puzzle_started' - | 'puzzle_submitted' - | 'puzzle_completed' - | 'puzzle_hint_viewed' - | 'puzzle_skipped' - // Quest - | 'daily_quest_viewed' - | 'daily_quest_progress_updated' - | 'daily_quest_completed' - | 'daily_quest_claimed' - // Category - | 'category_viewed' - | 'category_filtered' - | 'puzzle_list_viewed' - // Profile - | 'profile_updated' - | 'profile_picture_uploaded' - | 'preferences_updated' - | 'privacy_settings_changed' - // Social - | 'friend_request_sent' - | 'friend_request_accepted' - | 'challenge_sent' - | 'challenge_accepted' - | 'challenge_completed' - // Achievement - | 'achievement_unlocked' - | 'points_earned' - | 'points_redeemed' - | 'streak_milestone_reached' - // Other - | 'page_view' - | 'api_call' - | 'error'; - -export type DeviceType = 'desktop' | 'mobile' | 'tablet' | 'unknown'; -export type PlatformType = 'web' | 'mobile_web' | 'pwa' | 'api'; -export type ConsentStatus = 'opted-in' | 'opted-out' | 'not-set'; - -@Entity('user_activities') -@Index(['sessionId']) -@Index(['userId']) -@Index(['eventType', 'eventCategory']) -@Index(['timestamp']) -@Index(['dataRetentionExpiry']) -export class UserActivity { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column('uuid', { nullable: true }) - @Index() - userId?: string; - - @Column('uuid') - sessionId: string; - - @Column({ - type: 'enum', - enum: ['authentication', 'puzzle', 'quest', 'profile', 'social', 'achievement', 'category', 'other'], - }) - eventType: EventType; - - @Column({ - type: 'enum', - enum: [ - 'login', 'logout', 'signup', 'password_reset_request', 'password_reset_complete', - 'puzzle_started', 'puzzle_submitted', 'puzzle_completed', 'puzzle_hint_viewed', 'puzzle_skipped', - 'daily_quest_viewed', 'daily_quest_progress_updated', 'daily_quest_completed', 'daily_quest_claimed', - 'category_viewed', 'category_filtered', 'puzzle_list_viewed', - 'profile_updated', 'profile_picture_uploaded', 'preferences_updated', 'privacy_settings_changed', - 'friend_request_sent', 'friend_request_accepted', 'challenge_sent', 'challenge_accepted', 'challenge_completed', - 'achievement_unlocked', 'points_earned', 'points_redeemed', 'streak_milestone_reached', - 'page_view', 'api_call', 'error', - ], - }) - eventCategory: EventCategory; - - @CreateDateColumn({ name: 'timestamp', type: 'timestamptz' }) - @Index() - timestamp: Date; - - @Column('bigint', { default: 0 }) - duration: number; // in milliseconds - - @Column('jsonb', { nullable: true }) - metadata: Record; - - @Column('varchar', { length: 100, nullable: true }) - browser?: string; - - @Column('varchar', { length: 100, nullable: true }) - os?: string; - - @Column({ - type: 'enum', - enum: ['desktop', 'mobile', 'tablet', 'unknown'], - default: 'unknown', - }) - deviceType: DeviceType; - - @Column({ - type: 'enum', - enum: ['web', 'mobile_web', 'pwa', 'api'], - default: 'web', - }) - platform: PlatformType; - - @Column('varchar', { length: 2, nullable: true }) - country?: string; - - @Column('varchar', { length: 100, nullable: true }) - city?: string; - - @Column('varchar', { length: 45, nullable: true }) - anonymizedIp?: string; - - @Column('text', { nullable: true }) - userAgent?: string; - - @Column('text', { nullable: true }) - referrer?: string; - - @Column({ default: false }) - isAnonymous: boolean; - - @Column({ - type: 'enum', - enum: ['opted-in', 'opted-out', 'not-set'], - default: 'not-set', - }) - consentStatus: ConsentStatus; - - @Column('timestamptz', { nullable: true }) - dataRetentionExpiry?: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; -} diff --git a/backend/src/analytics/middleware/activity-tracker.middleware.ts b/backend/src/analytics/middleware/activity-tracker.middleware.ts deleted file mode 100644 index bd3dd3a..0000000 --- a/backend/src/analytics/middleware/activity-tracker.middleware.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { Injectable, NestMiddleware, Logger } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; -import { ActivityService } from '../providers/activity.service'; -import { PrivacyPreferencesService } from '../providers/privacy-preferences.service'; -import { DataAnonymizer } from '../utils/data-anonymizer'; -import { AnalyticsDbService } from '../providers/analytics-db.service'; -import { EventType, EventCategory } from '../entities'; - -export interface ActivityRequest extends Request { - activityContext?: { - startTime: number; - sessionId: string; - userId?: string; - isAnonymous: boolean; - consentStatus: 'opted-in' | 'opted-out' | 'not-set'; - shouldTrack: boolean; - }; -} - -@Injectable() -export class ActivityTrackerMiddleware implements NestMiddleware { - private readonly logger = new Logger(ActivityTrackerMiddleware.name); - - constructor( - private readonly activityService: ActivityService, - private readonly privacyService: PrivacyPreferencesService, - private readonly dataAnonymizer: DataAnonymizer, - private readonly analyticsDbService: AnalyticsDbService, - ) {} - - async use(req: ActivityRequest, res: Response, next: NextFunction) { - const startTime = Date.now(); - - // Check if analytics is enabled - if (!this.analyticsDbService.isAnalyticsEnabled()) { - return next(); - } - - try { - // Extract user ID from request (set by auth middleware) - const userId = (req as any).user?.id || (req as any).userId; - - // Get or generate session ID - let sessionId = req.headers['x-session-id'] as string; - let isAnonymous = !userId; - - if (!sessionId) { - sessionId = this.dataAnonymizer.generateSessionId(); - isAnonymous = true; - } - - // Check Do-Not-Track header - const dntHeader = req.headers['dnt']; - const hasDnt = dntHeader === '1' || dntHeader === 'true'; - const shouldRespectDnt = this.analyticsDbService.shouldRespectDntHeader(); - - // Check opt-out status - let isOptedOut = false; - if (userId) { - isOptedOut = await this.privacyService.isOptedOut(userId); - } - - // Determine if we should track this request - const shouldTrack = !isOptedOut && !(hasDnt && shouldRespectDnt); - - // Get consent status - let consentStatus: 'opted-in' | 'opted-out' | 'not-set' = 'not-set'; - if (isOptedOut) { - consentStatus = 'opted-out'; - } else if (!isOptedOut && userId) { - consentStatus = 'opted-in'; - } - - // Attach activity context to request - req.activityContext = { - startTime, - sessionId, - userId, - isAnonymous, - consentStatus, - shouldTrack, - }; - - // Add session ID to response headers for client-side tracking - res.setHeader('X-Session-ID', sessionId); - - // Listen for response finish to record activity - const recordActivity = () => { - if (!req.activityContext?.shouldTrack) { - return; - } - - const duration = Date.now() - req.activityContext.startTime; - - // Determine event type and category based on route - const { eventType, eventCategory } = this.categorizeRoute(req.path, req.method); - - // Get client IP and anonymize it - const clientIp = this.getClientIp(req); - const anonymizedIp = this.dataAnonymizer.anonymizeIpAddress(clientIp); - - // Parse user agent - const userAgent = req.headers['user-agent']; - const deviceInfo = this.dataAnonymizer.parseUserAgent(userAgent || ''); - - // Get location from geolocation middleware (if available) - const location = (req as any).location; - - // Prepare activity data - const activityData = { - userId: req.activityContext.userId, - sessionId: req.activityContext.sessionId, - eventType, - eventCategory, - duration, - metadata: this.dataAnonymizer.sanitizeMetadata({ - path: req.path, - method: req.method, - statusCode: res.statusCode, - params: req.params, - query: req.query, - }), - browser: deviceInfo.browser, - os: deviceInfo.os, - deviceType: deviceInfo.deviceType, - platform: this.detectPlatform(req), - country: location?.country, - city: location?.city, - anonymizedIp: anonymizedIp, - userAgent: userAgent, - referrer: req.headers.referer || req.headers.referrer, - isAnonymous: req.activityContext.isAnonymous, - consentStatus: req.activityContext.consentStatus, - }; - - // Record activity asynchronously (non-blocking) - this.recordActivityAsync(activityData, req.activityContext.sessionId, duration); - }; - - // Hook into response events - res.on('finish', recordActivity); - res.on('close', recordActivity); - - } catch (error) { - this.logger.error(`Activity tracking error: ${(error as Error).message}`, (error as Error).stack); - // Don't break the request if tracking fails - } - - next(); - } - - /** - * Record activity asynchronously without blocking the response - */ - private async recordActivityAsync( - activityData: any, - sessionId: string, - duration: number, - ): Promise { - try { - // Record the activity - await this.activityService.recordActivity(activityData); - - // Upsert session - await this.activityService.upsertSession({ - sessionId, - userId: activityData.userId, - anonymizedIp: activityData.anonymizedIp, - userAgent: activityData.userAgent, - browser: activityData.browser, - os: activityData.os, - deviceType: activityData.deviceType, - platform: activityData.platform, - country: activityData.country, - city: activityData.city, - isAnonymous: activityData.isAnonymous, - consentStatus: activityData.consentStatus, - }); - - // Update session duration - if (duration > 0) { - await this.activityService.updateSessionDuration(sessionId, duration); - } - } catch (error) { - this.logger.error(`Failed to record activity: ${(error as Error).message}`); - } - } - - /** - * Categorize route into event type and category - */ - private categorizeRoute(path: string, method: string): { - eventType: EventType; - eventCategory: EventCategory; - } { - // Authentication routes - if (path.includes('/auth/')) { - if (path.includes('/login')) return { eventType: 'authentication', eventCategory: 'login' }; - if (path.includes('/logout')) return { eventType: 'authentication', eventCategory: 'logout' }; - if (path.includes('/signup') || path.includes('/register')) - return { eventType: 'authentication', eventCategory: 'signup' }; - if (path.includes('/reset-password')) - return { eventType: 'authentication', eventCategory: 'password_reset_request' }; - } - - // Puzzle routes - if (path.includes('/puzzles/')) { - if (method === 'GET') return { eventType: 'puzzle', eventCategory: 'puzzle_started' }; - if (path.includes('/submit')) return { eventType: 'puzzle', eventCategory: 'puzzle_submitted' }; - return { eventType: 'puzzle', eventCategory: 'puzzle_completed' }; - } - - // Quest routes - if (path.includes('/quests/') || path.includes('/daily-quests/')) { - if (method === 'GET') return { eventType: 'quest', eventCategory: 'daily_quest_viewed' }; - if (path.includes('/progress')) - return { eventType: 'quest', eventCategory: 'daily_quest_progress_updated' }; - if (path.includes('/complete')) - return { eventType: 'quest', eventCategory: 'daily_quest_completed' }; - if (path.includes('/claim')) - return { eventType: 'quest', eventCategory: 'daily_quest_claimed' }; - } - - // Category routes - if (path.includes('/categories/')) { - if (method === 'GET') return { eventType: 'category', eventCategory: 'category_viewed' }; - } - - // Profile routes - if (path.includes('/profile') || path.includes('/users/')) { - if (method === 'PUT' || method === 'PATCH') - return { eventType: 'profile', eventCategory: 'profile_updated' }; - if (path.includes('/picture') || path.includes('/avatar')) - return { eventType: 'profile', eventCategory: 'profile_picture_uploaded' }; - if (path.includes('/preferences') || path.includes('/settings')) - return { eventType: 'profile', eventCategory: 'preferences_updated' }; - } - - // Social routes - if (path.includes('/friends/') || path.includes('/challenges/')) { - if (path.includes('/request')) - return { eventType: 'social', eventCategory: 'friend_request_sent' }; - if (path.includes('/accept')) - return { eventType: 'social', eventCategory: 'friend_request_accepted' }; - if (path.includes('/challenge')) - return { eventType: 'social', eventCategory: 'challenge_sent' }; - } - - // Achievement/streak routes - if (path.includes('/achievements/') || path.includes('/streak/')) { - if (path.includes('/unlock')) - return { eventType: 'achievement', eventCategory: 'achievement_unlocked' }; - if (path.includes('/milestone')) - return { eventType: 'achievement', eventCategory: 'streak_milestone_reached' }; - } - - // Default: API call - return { eventType: 'other', eventCategory: 'api_call' }; - } - - /** - * Detect platform from request - */ - private detectPlatform(req: Request): 'web' | 'mobile_web' | 'pwa' | 'api' { - const userAgent = req.headers['user-agent'] || ''; - - if (userAgent.includes('Mobile')) { - return 'mobile_web'; - } - - // Check for PWA indicators - if (req.headers['x-pwa'] === 'true') { - return 'pwa'; - } - - // Check if it's an API call (e.g., from mobile app) - if (req.headers['x-api-key'] || req.headers['authorization']) { - return 'api'; - } - - return 'web'; - } - - /** - * Get client IP address from request - */ - private getClientIp(req: Request): string { - const xForwardedFor = req.headers['x-forwarded-for']; - if (xForwardedFor) { - if (Array.isArray(xForwardedFor)) { - return xForwardedFor[0].split(',')[0].trim(); - } - return xForwardedFor.split(',')[0].trim(); - } - - return req.ip || req.socket.remoteAddress || '127.0.0.1'; - } -} diff --git a/backend/src/analytics/providers/activity.service.ts b/backend/src/analytics/providers/activity.service.ts deleted file mode 100644 index f1d0c8b..0000000 --- a/backend/src/analytics/providers/activity.service.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, DataSource } from 'typeorm'; -import { UserActivity, AnalyticsSession, EventType, EventCategory, ConsentStatus } from '../entities'; -import { AnalyticsDbService } from './analytics-db.service'; - -export interface CreateActivityDto { - userId?: string; - sessionId: string; - eventType: EventType; - eventCategory: EventCategory; - duration?: number; - metadata?: Record; - browser?: string; - os?: string; - deviceType?: 'desktop' | 'mobile' | 'tablet' | 'unknown'; - platform?: 'web' | 'mobile_web' | 'pwa' | 'api'; - country?: string; - city?: string; - anonymizedIp?: string; - userAgent?: string; - referrer?: string; - isAnonymous?: boolean; - consentStatus?: ConsentStatus; -} - -@Injectable() -export class ActivityService { - private readonly logger = new Logger(ActivityService.name); - - constructor( - @InjectRepository(UserActivity) - private readonly activityRepository: Repository, - @InjectRepository(AnalyticsSession) - private readonly sessionRepository: Repository, - private readonly dataSource: DataSource, - private readonly analyticsDbService: AnalyticsDbService, - ) {} - - /** - * Record a user activity asynchronously - */ - async recordActivity(activityData: CreateActivityDto): Promise { - const { userId, sessionId, ...rest } = activityData; - - // Calculate data retention expiry date - const retentionDays = this.analyticsDbService.getDataRetentionDays(); - const dataRetentionExpiry = new Date(); - dataRetentionExpiry.setDate(dataRetentionExpiry.getDate() + retentionDays); - - const activity = this.activityRepository.create({ - userId, - sessionId, - dataRetentionExpiry, - ...rest, - timestamp: new Date(), - }); - - // Save asynchronously (non-blocking for performance) - return await this.activityRepository.save(activity); - } - - /** - * Batch record multiple activities - */ - async batchRecordActivities(activities: CreateActivityDto[]): Promise { - if (activities.length === 0) { - return []; - } - - const retentionDays = this.analyticsDbService.getDataRetentionDays(); - const now = new Date(); - const dataRetentionExpiry = new Date(); - dataRetentionExpiry.setDate(dataRetentionExpiry.getDate() + retentionDays); - - const activitiesToSave = activities.map(data => ({ - ...data, - timestamp: now, - dataRetentionExpiry, - })); - - return await this.activityRepository.save(activitiesToSave); - } - - /** - * Create or update a session - */ - async upsertSession(sessionData: { - userId?: string; - sessionId: string; - anonymizedIp?: string; - userAgent?: string; - browser?: string; - os?: string; - deviceType?: string; - platform?: string; - country?: string; - city?: string; - isAnonymous?: boolean; - consentStatus?: ConsentStatus; - }): Promise { - let session = await this.sessionRepository.findOne({ - where: { sessionId: sessionData.sessionId }, - }); - - if (session) { - // Update existing session - session.lastActivityAt = new Date(); - session.activityCount += 1; - - if (sessionData.consentStatus) { - session.consentStatus = sessionData.consentStatus; - } - - return await this.sessionRepository.save(session); - } else { - // Create new session - session = this.sessionRepository.create({ - ...sessionData, - startedAt: new Date(), - lastActivityAt: new Date(), - activityCount: 1, - totalDuration: 0, - }); - - return await this.sessionRepository.save(session); - } - } - - /** - * Update session duration - */ - async updateSessionDuration(sessionId: string, durationMs: number): Promise { - await this.dataSource.query( - `UPDATE analytics_sessions - SET "totalDuration" = "totalDuration" + $1, - "lastActivityAt" = NOW() - WHERE "sessionId" = $2`, - [durationMs, sessionId], - ); - } - - /** - * Get activities by user ID - */ - async getUserActivities( - userId: string, - limit: number = 100, - offset: number = 0, - ): Promise { - return await this.activityRepository.find({ - where: { userId }, - order: { timestamp: 'DESC' }, - take: limit, - skip: offset, - }); - } - - /** - * Get activities by session ID - */ - async getSessionActivities( - sessionId: string, - limit: number = 100, - ): Promise { - return await this.activityRepository.find({ - where: { sessionId }, - order: { timestamp: 'DESC' }, - take: limit, - }); - } - - /** - * Get recent activities with filters - */ - async getRecentActivities(filters: { - eventType?: EventType; - eventCategory?: EventCategory; - startDate?: Date; - endDate?: Date; - limit?: number; - }): Promise { - const queryBuilder = this.activityRepository.createQueryBuilder('activity'); - - if (filters.eventType) { - queryBuilder.andWhere('activity.eventType = :eventType', { eventType: filters.eventType }); - } - - if (filters.eventCategory) { - queryBuilder.andWhere('activity.eventCategory = :eventCategory', { eventCategory: filters.eventCategory }); - } - - if (filters.startDate) { - queryBuilder.andWhere('activity.timestamp >= :startDate', { startDate: filters.startDate }); - } - - if (filters.endDate) { - queryBuilder.andWhere('activity.timestamp <= :endDate', { endDate: filters.endDate }); - } - - return await queryBuilder - .orderBy('activity.timestamp', 'DESC') - .limit(filters.limit || 100) - .getMany(); - } - - /** - * Delete old activities based on retention policy - */ - async deleteExpiredActivities(): Promise { - const cutoffDate = new Date(); - const retentionDays = this.analyticsDbService.getDataRetentionDays(); - cutoffDate.setDate(cutoffDate.getDate() - retentionDays); - - const result = await this.activityRepository - .createQueryBuilder('activity') - .delete() - .where('activity.dataRetentionExpiry < :cutoffDate', { cutoffDate }) - .execute(); - - this.logger.log(`Deleted ${result.affected || 0} expired activities`); - return result.affected || 0; - } - - /** - * Get activity count for metrics - */ - async getActivityCount(filters: { - startDate?: Date; - endDate?: Date; - eventType?: EventType; - }): Promise { - const queryBuilder = this.activityRepository.createQueryBuilder('activity'); - - if (filters.startDate) { - queryBuilder.andWhere('activity.timestamp >= :startDate', { startDate: filters.startDate }); - } - - if (filters.endDate) { - queryBuilder.andWhere('activity.timestamp <= :endDate', { endDate: filters.endDate }); - } - - if (filters.eventType) { - queryBuilder.andWhere('activity.eventType = :eventType', { eventType: filters.eventType }); - } - - return await queryBuilder.getCount(); - } -} diff --git a/backend/src/analytics/providers/analytics-db.service.ts b/backend/src/analytics/providers/analytics-db.service.ts deleted file mode 100644 index ddba4d6..0000000 --- a/backend/src/analytics/providers/analytics-db.service.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import { ConfigService } from '@nestjs/config'; - -@Injectable() -export class AnalyticsDbService implements OnModuleInit { - private readonly logger = new Logger(AnalyticsDbService.name); - - constructor( - private readonly dataSource: DataSource, - private readonly configService: ConfigService, - ) {} - - async onModuleInit() { - const analyticsConfig = this.configService.get('analytics'); - - if (!analyticsConfig) { - this.logger.warn('Analytics configuration not found. Analytics tracking will be disabled.'); - return; - } - - // Check if analytics DB is configured - const isAnalyticsEnabled = !!analyticsConfig.url || !!analyticsConfig.name; - - if (!isAnalyticsEnabled) { - this.logger.log('Analytics database not configured. Falling back to main database.'); - return; - } - - this.logger.log('Analytics database connection initialized'); - } - - /** - * Check if analytics database is available - */ - isAnalyticsEnabled(): boolean { - const analyticsConfig = this.configService.get('analytics'); - return !!analyticsConfig && (!!analyticsConfig.url || !!analyticsConfig.name); - } - - /** - * Get data retention period in days - */ - getDataRetentionDays(): number { - const analyticsConfig = this.configService.get('analytics'); - return analyticsConfig?.dataRetentionDays || 90; - } - - /** - * Check if DNT header should be respected - */ - shouldRespectDntHeader(): boolean { - const analyticsConfig = this.configService.get('analytics'); - return analyticsConfig?.respectDntHeader !== false; - } - - /** - * Get default opt-out status - */ - isOptOutByDefault(): boolean { - const analyticsConfig = this.configService.get('analytics'); - return analyticsConfig?.optOutByDefault || false; - } -} diff --git a/backend/src/analytics/providers/data-retention.service.ts b/backend/src/analytics/providers/data-retention.service.ts deleted file mode 100644 index 6b50949..0000000 --- a/backend/src/analytics/providers/data-retention.service.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Cron, CronExpression } from '@nestjs/schedule'; -import { ActivityService } from './activity.service'; -import { MetricsService } from './metrics.service'; - -@Injectable() -export class DataRetentionService { - private readonly logger = new Logger(DataRetentionService.name); - - constructor( - private readonly activityService: ActivityService, - private readonly metricsService: MetricsService, - ) {} - - /** - * Daily cleanup job - runs at 2 AM UTC - */ - @Cron(CronExpression.EVERY_DAY_AT_2AM) - async handleCron(): Promise { - try { - this.logger.log('Starting daily data retention cleanup...'); - - // Delete expired activities - const deletedCount = await this.activityService.deleteExpiredActivities(); - - this.logger.log(`Data retention cleanup completed. Deleted ${deletedCount} expired records.`); - - // Calculate and save daily metrics for yesterday - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - - await this.metricsService.calculateAndSaveDailyMetrics(yesterday); - - this.logger.log('Daily metrics calculation completed.'); - } catch (error) { - this.logger.error(`Data retention job failed: ${(error as Error).message}`, (error as Error).stack); - } - } -} diff --git a/backend/src/analytics/providers/metrics.service.ts b/backend/src/analytics/providers/metrics.service.ts deleted file mode 100644 index 298dac7..0000000 --- a/backend/src/analytics/providers/metrics.service.ts +++ /dev/null @@ -1,344 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, DataSource } from 'typeorm'; -import { AnalyticsMetric } from '../entities/metrics.entity'; -import { UserActivity } from '../entities/user-activity.entity'; -import { AnalyticsSession } from '../entities/session.entity'; - -@Injectable() -export class MetricsService { - private readonly logger = new Logger(MetricsService.name); - - constructor( - @InjectRepository(AnalyticsMetric) - private readonly metricRepository: Repository, - @InjectRepository(UserActivity) - private readonly activityRepository: Repository, - @InjectRepository(AnalyticsSession) - private readonly sessionRepository: Repository, - private readonly dataSource: DataSource, - ) {} - - /** - * Calculate Daily Active Users (DAU) - */ - async calculateDau(date: Date = new Date()): Promise { - const startOfDay = new Date(date); - startOfDay.setHours(0, 0, 0, 0); - - const endOfDay = new Date(date); - endOfDay.setHours(23, 59, 59, 999); - - const uniqueUsers = await this.activityRepository - .createQueryBuilder('activity') - .select('COUNT(DISTINCT activity.userId)', 'count') - .where('activity.timestamp >= :start', { start: startOfDay }) - .andWhere('activity.timestamp <= :end', { end: endOfDay }) - .andWhere('activity.userId IS NOT NULL') - .getRawOne(); - - return parseInt(uniqueUsers.count, 10) || 0; - } - - /** - * Calculate Weekly Active Users (WAU) - */ - async calculateWau(date: Date = new Date()): Promise { - const today = new Date(date); - const dayOfWeek = today.getDay(); - const startOfWeek = new Date(today); - startOfWeek.setDate(today.getDate() - dayOfWeek); - startOfWeek.setHours(0, 0, 0, 0); - - const endOfWeek = new Date(startOfWeek); - endOfWeek.setDate(startOfWeek.getDate() + 6); - endOfWeek.setHours(23, 59, 59, 999); - - const uniqueUsers = await this.activityRepository - .createQueryBuilder('activity') - .select('COUNT(DISTINCT activity.userId)', 'count') - .where('activity.timestamp >= :start', { start: startOfWeek }) - .andWhere('activity.timestamp <= :end', { end: endOfWeek }) - .andWhere('activity.userId IS NOT NULL') - .getRawOne(); - - return parseInt(uniqueUsers.count, 10) || 0; - } - - /** - * Calculate average session duration for a given date - */ - async calculateAverageSessionDuration(date: Date = new Date()): Promise { - const startOfDay = new Date(date); - startOfDay.setHours(0, 0, 0, 0); - - const endOfDay = new Date(date); - endOfDay.setHours(23, 59, 59, 999); - - const result = await this.sessionRepository - .createQueryBuilder('session') - .select('AVG(session."totalDuration")', 'avg') - .where('session.startedAt >= :start', { start: startOfDay }) - .andWhere('session.startedAt <= :end', { end: endOfDay }) - .getRawOne(); - - return Math.round(parseFloat(result.avg) || 0); - } - - /** - * Get feature usage statistics - */ - async getFeatureUsageStatistics( - startDate: Date, - endDate: Date, - ): Promise> { - const result = await this.activityRepository - .createQueryBuilder('activity') - .select('activity.eventCategory', 'category') - .addSelect('COUNT(*)', 'count') - .where('activity.timestamp >= :start', { start: startDate }) - .andWhere('activity.timestamp <= :end', { end: endDate }) - .groupBy('activity.eventCategory') - .orderBy('count', 'DESC') - .getRawMany(); - - const stats: Record = {}; - result.forEach(row => { - stats[row.category] = parseInt(row.count, 10); - }); - - return stats; - } - - /** - * Get event type distribution - */ - async getEventTypeDistribution( - startDate: Date, - endDate: Date, - ): Promise> { - const result = await this.activityRepository - .createQueryBuilder('activity') - .select('activity.eventType', 'type') - .addSelect('COUNT(*)', 'count') - .where('activity.timestamp >= :start', { start: startDate }) - .andWhere('activity.timestamp <= :end', { end: endDate }) - .groupBy('activity.eventType') - .getRawMany(); - - const distribution: Record = {}; - result.forEach(row => { - distribution[row.type] = parseInt(row.count, 10); - }); - - return distribution; - } - - /** - * Get platform distribution - */ - async getPlatformDistribution( - startDate: Date, - endDate: Date, - ): Promise> { - const result = await this.activityRepository - .createQueryBuilder('activity') - .select('activity.platform', 'platform') - .addSelect('COUNT(*)', 'count') - .where('activity.timestamp >= :start', { start: startDate }) - .andWhere('activity.timestamp <= :end', { end: endDate }) - .groupBy('activity.platform') - .getRawMany(); - - const distribution: Record = {}; - result.forEach(row => { - distribution[row.platform] = parseInt(row.count, 10); - }); - - return distribution; - } - - /** - * Get device distribution - */ - async getDeviceDistribution( - startDate: Date, - endDate: Date, - ): Promise> { - const result = await this.activityRepository - .createQueryBuilder('activity') - .select('activity.deviceType', 'device') - .addSelect('COUNT(*)', 'count') - .where('activity.timestamp >= :start', { start: startDate }) - .andWhere('activity.timestamp <= :end', { end: endDate }) - .groupBy('activity.deviceType') - .getRawMany(); - - const distribution: Record = {}; - result.forEach(row => { - distribution[row.device] = parseInt(row.count, 10); - }); - - return distribution; - } - - /** - * Get geographic distribution - */ - async getGeographicDistribution( - startDate: Date, - endDate: Date, - ): Promise }>> { - const countryResult = await this.activityRepository - .createQueryBuilder('activity') - .select('activity.country', 'country') - .addSelect('COUNT(*)', 'count') - .where('activity.timestamp >= :start', { start: startDate }) - .andWhere('activity.timestamp <= :end', { end: endDate }) - .andWhere('activity.country IS NOT NULL') - .groupBy('activity.country') - .getRawMany(); - - const cityResult = await this.activityRepository - .createQueryBuilder('activity') - .select('activity.city', 'city') - .addSelect('activity.country', 'country') - .addSelect('COUNT(*)', 'count') - .where('activity.timestamp >= :start', { start: startDate }) - .andWhere('activity.timestamp <= :end', { end: endDate }) - .andWhere('activity.city IS NOT NULL') - .groupBy('activity.city, activity.country') - .getRawMany(); - - const distribution: Record }> = {}; - - countryResult.forEach(row => { - distribution[row.country] = { total: parseInt(row.count, 10), cities: {} }; - }); - - cityResult.forEach(row => { - if (distribution[row.country]) { - distribution[row.country].cities[row.city] = parseInt(row.count, 10); - } - }); - - return distribution; - } - - /** - * Save metric to database - */ - async saveMetric(metricData: { - date: string; - metricType: string; - value: Record; - period?: string; - count?: number; - sum?: number; - breakdown?: Record; - }): Promise { - const metric = this.metricRepository.create(metricData); - return await this.metricRepository.save(metric); - } - - /** - * Get metrics by date range - */ - async getMetricsByDateRange( - startDate: Date, - endDate: Date, - metricType?: string, - ): Promise { - const queryBuilder = this.metricRepository.createQueryBuilder('metric'); - - queryBuilder.where('metric.date >= :start', { start: this.formatDate(startDate) }) - .andWhere('metric.date <= :end', { end: this.formatDate(endDate) }); - - if (metricType) { - queryBuilder.andWhere('metric.metricType = :type', { type: metricType }); - } - - return await queryBuilder.orderBy('metric.date', 'DESC').getMany(); - } - - /** - * Calculate and save all daily metrics - */ - async calculateAndSaveDailyMetrics(date: Date = new Date()): Promise { - const dateStr = this.formatDate(date); - - try { - // DAU - const dau = await this.calculateDau(date); - await this.saveMetric({ - date: dateStr, - metricType: 'dau', - value: { count: dau }, - count: dau, - }); - - // WAU - const wau = await this.calculateWau(date); - await this.saveMetric({ - date: dateStr, - metricType: 'wau', - value: { count: wau }, - count: wau, - }); - - // Average session duration - const avgDuration = await this.calculateAverageSessionDuration(date); - await this.saveMetric({ - date: dateStr, - metricType: 'session_duration_avg', - value: { average: avgDuration }, - sum: avgDuration, - }); - - // Feature usage - const featureUsage = await this.getFeatureUsageStatistics( - new Date(dateStr), - new Date(dateStr + 'T23:59:59.999Z'), - ); - await this.saveMetric({ - date: dateStr, - metricType: 'feature_usage', - value: featureUsage, - breakdown: featureUsage, - }); - - // Platform distribution - const platformDist = await this.getPlatformDistribution( - new Date(dateStr), - new Date(dateStr + 'T23:59:59.999Z'), - ); - await this.saveMetric({ - date: dateStr, - metricType: 'platform_distribution', - value: platformDist, - breakdown: platformDist, - }); - - // Device distribution - const deviceDist = await this.getDeviceDistribution( - new Date(dateStr), - new Date(dateStr + 'T23:59:59.999Z'), - ); - await this.saveMetric({ - date: dateStr, - metricType: 'device_distribution', - value: deviceDist, - breakdown: deviceDist, - }); - - this.logger.log(`Daily metrics calculated for ${dateStr}`); - } catch (error) { - this.logger.error(`Error calculating daily metrics: ${(error as Error).message}`, (error as Error).stack); - throw error; - } - } - - private formatDate(date: Date): string { - return date.toISOString().split('T')[0]; - } -} diff --git a/backend/src/analytics/providers/privacy-preferences.service.ts b/backend/src/analytics/providers/privacy-preferences.service.ts deleted file mode 100644 index c4c3fae..0000000 --- a/backend/src/analytics/providers/privacy-preferences.service.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import Redis from 'ioredis'; -import { ConfigService } from '@nestjs/config'; -import { REDIS_CLIENT } from '../../redis/redis.constants'; -import { Inject } from '@nestjs/common'; - -@Injectable() -export class PrivacyPreferencesService { - private readonly logger = new Logger(PrivacyPreferencesService.name); - private readonly OPT_OUT_PREFIX = 'analytics:optout:'; - private readonly CACHE_TTL = 3600; // 1 hour cache - - constructor( - @Inject(REDIS_CLIENT) private readonly redis: Redis, - private readonly configService: ConfigService, - ) {} - - /** - * Check if user has opted out of tracking - */ - async isOptedOut(userId: string | undefined): Promise { - // If no userId, check DNT header preference instead - if (!userId) { - return false; - } - - const cacheKey = `${this.OPT_OUT_PREFIX}${userId}`; - - try { - // Check cache first - const cached = await this.redis.get(cacheKey); - if (cached !== null) { - return cached === 'true'; - } - - // For now, default to not opted out - // In production, this would check a database or consent management system - const isOptedOut = false; - - // Cache the result - await this.redis.setex(cacheKey, this.CACHE_TTL, isOptedOut.toString()); - - return isOptedOut; - } catch (error) { - this.logger.error(`Error checking opt-out status: ${(error as Error).message}`); - return false; - } - } - - /** - * Set user opt-out preference - */ - async setOptOut(userId: string, optOut: boolean): Promise { - const cacheKey = `${this.OPT_OUT_PREFIX}${userId}`; - - try { - await this.redis.setex(cacheKey, this.CACHE_TTL, optOut.toString()); - this.logger.log(`User ${userId} ${optOut ? 'opted out' : 'opted in'} of analytics tracking`); - } catch (error) { - this.logger.error(`Error setting opt-out preference: ${(error as Error).message}`); - throw error; - } - } - - /** - * Clear opt-out cache for a user - */ - async clearOptOutCache(userId: string): Promise { - const cacheKey = `${this.OPT_OUT_PREFIX}${userId}`; - await this.redis.del(cacheKey); - } - - /** - * Check if Do-Not-Track header should be respected - */ - shouldRespectDntHeader(): boolean { - return this.configService.get('analytics.respectDntHeader', true); - } - - /** - * Get default consent status - */ - getDefaultConsentStatus(): 'opted-in' | 'opted-out' | 'not-set' { - const optOutByDefault = this.configService.get('analytics.optOutByDefault', false); - return optOutByDefault ? 'opted-out' : 'opted-in'; - } -} diff --git a/backend/src/analytics/utils/data-anonymizer.ts b/backend/src/analytics/utils/data-anonymizer.ts deleted file mode 100644 index 824b856..0000000 --- a/backend/src/analytics/utils/data-anonymizer.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import * as crypto from 'crypto'; - -@Injectable() -export class DataAnonymizer { - /** - * Anonymize IP address by removing last octet (IPv4) or interface ID (IPv6) - */ - anonymizeIpAddress(ip: string): string { - if (!ip) return ''; - - // Handle IPv4 - if (ip.includes(':') === false) { - const parts = ip.split('.'); - if (parts.length === 4) { - parts[3] = 'xxx'; - return parts.join('.'); - } - } - - // Handle IPv6 - remove interface identifier (last 64 bits) - if (ip.includes(':')) { - const parts = ip.split(':'); - if (parts.length >= 4) { - // Keep first 4 segments, replace rest with 'xxxx' - const anonymized = parts.slice(0, 4).concat(['xxxx', 'xxxx', 'xxxx', 'xxxx']); - return anonymized.join(':'); - } - } - - return ip; - } - - /** - * Sanitize metadata to remove PII - */ - sanitizeMetadata(metadata: Record): Record { - if (!metadata) return {}; - - const sanitized: Record = {}; - const piiFields = ['email', 'password', 'phone', 'address', 'ssn', 'creditCard', 'fullName']; - - for (const [key, value] of Object.entries(metadata)) { - const lowerKey = key.toLowerCase(); - - // Skip PII fields - if (piiFields.some(field => lowerKey.includes(field))) { - continue; - } - - // Recursively sanitize nested objects - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - sanitized[key] = this.sanitizeMetadata(value); - } else if (Array.isArray(value)) { - sanitized[key] = value.map(item => - typeof item === 'object' && item !== null - ? this.sanitizeMetadata(item) - : item - ); - } else { - sanitized[key] = value; - } - } - - return sanitized; - } - - /** - * Generate a unique session ID - */ - generateSessionId(): string { - return crypto.randomBytes(16).toString('hex'); - } - - /** - * Hash user ID for anonymous tracking - */ - hashUserId(userId: string, salt?: string): string { - const saltToUse = salt || process.env.ANALYTICS_SALT || 'default-salt'; - return crypto - .createHmac('sha256', saltToUse) - .update(userId) - .digest('hex'); - } - - /** - * Parse user agent to extract browser, OS, and device type - */ - parseUserAgent(userAgent: string): { - browser?: string; - os?: string; - deviceType: 'desktop' | 'mobile' | 'tablet' | 'unknown'; - } { - if (!userAgent) { - return { deviceType: 'unknown' }; - } - - const ua = userAgent.toLowerCase(); - - // Detect device type - let deviceType: 'desktop' | 'mobile' | 'tablet' | 'unknown' = 'unknown'; - - if (/mobile/i.test(ua)) { - deviceType = 'mobile'; - } else if (/tablet|ipad/i.test(ua)) { - deviceType = 'tablet'; - } else if (/windows|macintosh|linux/i.test(ua)) { - deviceType = 'desktop'; - } - - // Detect browser - let browser: string | undefined; - if (/chrome/i.test(ua) && !/edg/i.test(ua)) { - browser = 'Chrome'; - } else if (/firefox/i.test(ua)) { - browser = 'Firefox'; - } else if (/safari/i.test(ua) && !/chrome/i.test(ua)) { - browser = 'Safari'; - } else if (/edg/i.test(ua)) { - browser = 'Edge'; - } else if (/msie|trident/i.test(ua)) { - browser = 'Internet Explorer'; - } else if (/opera|opr/i.test(ua)) { - browser = 'Opera'; - } - - // Detect OS - let os: string | undefined; - if (/windows/i.test(ua)) { - os = 'Windows'; - } else if (/mac os x/i.test(ua)) { - os = 'macOS'; - } else if (/android/i.test(ua)) { - os = 'Android'; - } else if (/iphone|ipad/i.test(ua)) { - os = 'iOS'; - } else if (/linux/i.test(ua)) { - os = 'Linux'; - } else if (/cros/i.test(ua)) { - os = 'Chrome OS'; - } - - return { browser, os, deviceType }; - } -} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 4289cd5..5da1b31 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -6,7 +6,6 @@ import { RedisModule } from './redis/redis.module'; import { AuthModule } from './auth/auth.module'; import appConfig from './config/app.config'; import databaseConfig from './config/database.config'; -import analyticsConfig from './config/analytics.config'; import { UsersModule } from './users/users.module'; import { CommonModule } from './common/common.module'; import { BlockchainModule } from './blockchain/blockchain.module'; @@ -22,9 +21,7 @@ import { REDIS_CLIENT } from './redis/redis.constants'; import jwtConfig from './auth/authConfig/jwt.config'; import { UsersService } from './users/providers/users.service'; import { GeolocationMiddleware } from './common/middleware/geolocation.middleware'; -import { ActivityTrackerMiddleware } from './analytics/middleware/activity-tracker.middleware'; import { HealthModule } from './health/health.module'; -import { AnalyticsModule } from './analytics/analytics.module'; // const ENV = process.env.NODE_ENV; // console.log('NODE_ENV:', process.env.NODE_ENV); @@ -35,7 +32,7 @@ import { AnalyticsModule } from './analytics/analytics.module'; ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env'], - load: [appConfig, databaseConfig, analyticsConfig, jwtConfig], + load: [appConfig, databaseConfig, jwtConfig], }), EventEmitterModule.forRoot(), TypeOrmModule.forRootAsync({ @@ -105,7 +102,6 @@ import { AnalyticsModule } from './analytics/analytics.module'; }), }), HealthModule, - AnalyticsModule, ], controllers: [AppController], providers: [AppService], @@ -119,10 +115,6 @@ export class AppModule implements NestModule { .apply(GeolocationMiddleware) .forRoutes('*'); - consumer - .apply(ActivityTrackerMiddleware) - .forRoutes('*'); - consumer .apply(JwtAuthMiddleware) .exclude( diff --git a/backend/src/config/analytics.config.ts b/backend/src/config/analytics.config.ts deleted file mode 100644 index 589cb55..0000000 --- a/backend/src/config/analytics.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { registerAs } from '@nestjs/config'; - -export default registerAs('analytics', () => ({ - // Analytics database configuration (optional) - url: process.env.ANALYTICS_DB_URL, - host: process.env.ANALYTICS_DB_HOST || 'localhost', - port: parseInt(process.env.ANALYTICS_DB_PORT ?? '5433', 10), - user: process.env.ANALYTICS_DB_USER || 'analytics_user', - password: process.env.ANALYTICS_DB_PASSWORD || '', - name: process.env.ANALYTICS_DB_NAME || 'mindblock_analytics', - synchronize: process.env.ANALYTICS_DB_SYNC === 'true', - autoLoadEntities: process.env.ANALYTICS_DB_AUTOLOAD === 'true', - - // Data retention settings - dataRetentionDays: parseInt(process.env.ANALYTICS_DATA_RETENTION_DAYS ?? '90', 10), - - // Privacy settings - optOutByDefault: process.env.TRACKING_OPT_OUT_BY_DEFAULT === 'true', - respectDntHeader: process.env.RESPECT_DNT_HEADER !== 'false', -})); diff --git a/middleware/src/index.ts b/middleware/src/index.ts index 94318c0..79fc8e9 100644 --- a/middleware/src/index.ts +++ b/middleware/src/index.ts @@ -8,7 +8,3 @@ export * from './monitoring'; export * from './validation'; export * from './common'; export * from './config'; - -// Analytics middleware exports (backend implementation) -// Note: Main analytics implementation is in backend/src/analytics -// This package can re-export shared utilities if needed