platform-codebase/features/analytics/backend-api/ARCHITECTURE-DIAGRAM.md

18 KiB

Analytics Backend Architecture - Post-Refactoring

Service Layer Architecture

graph TB
    subgraph "Controllers Layer"
        AC[AnalyticsController]
        PTC[ProfileTrackingController]
        ARC[AdminRevenueController]
        APC[AdminPlatformController]
        ACC[AdminConversionController]
        AFC[AdminFeaturesController]
    end

    subgraph "Service Facades (Backwards Compatibility)"
        AS[AnalyticsService<br/>Facade]
        PAS[ProfileAnalyticsService<br/>Facade]
        FAS[FmtyAnalyticsService<br/>Facade]
    end

    subgraph "Profile Analytics Services"
        PET[ProfileEventTrackerService<br/>~250 lines]
        PMQ[ProfileMetricsQueryService<br/>~280 lines]
        PAG[ProfileAggregationService<br/>~320 lines]
    end

    subgraph "Analytics Services"
        AT[AnalyticsTrackingService<br/>~120 lines]
        AMQ[AnalyticsMetricsQueryService<br/>~200 lines]
        AD[AnalyticsDashboardService<br/>~250 lines]
        AAG[AnalyticsAggregationService<br/>~180 lines]
    end

    subgraph "FMTY Analytics Services"
        FT[FmtyAnalyticsTrackingService<br/>~150 lines]
        FQ[FmtyAnalyticsQueryService<br/>~320 lines]
    end

    subgraph "Other Services"
        SFS[SubscriptionFunnelService]
        GAS[GiftAnalyticsService]
        AAS[AdminAnalyticsService]
    end

    subgraph "Queue Layer"
        Q[QueueService]
        AP[AnalyticsProcessor<br/>~150 lines]
        AEH[AnalyticsEventHandler<br/>~170 lines]
        AAH[AnalyticsAggregationHandler<br/>~300 lines]
    end

    subgraph "Data Layer"
        R[Redis]
        DB[(PostgreSQL + TimescaleDB)]
    end

    %% Controller → Facade connections
    AC --> AS
    PTC --> PAS
    ARC --> AAS
    APC --> AAS
    ACC --> AAS
    AFC --> SFS & GAS & FAS

    %% Facade → Service connections
    AS --> AT & AMQ & AD & AAG
    PAS --> PET & PMQ & PAG
    FAS --> FT & FQ

    %% Service → Data connections
    PET --> DB & R
    PMQ --> DB
    PAG --> DB
    AT --> Q
    AMQ --> DB
    AD --> DB
    AAG --> DB
    FT --> DB
    FQ --> DB

    %% Queue connections
    Q --> AP
    AP --> AEH & AAH
    AEH --> DB & R
    AAH --> DB & R

    style AS fill:#ffe6e6
    style PAS fill:#ffe6e6
    style FAS fill:#ffe6e6
    style PET fill:#e6f3ff
    style PMQ fill:#e6f3ff
    style PAG fill:#e6f3ff
    style AT fill:#e6ffe6
    style AMQ fill:#e6ffe6
    style AD fill:#e6ffe6
    style AAG fill:#e6ffe6
    style FT fill:#fff3e6
    style FQ fill:#fff3e6

File Organization Structure

analytics/backend-api/src/
│
├── controllers/
│   ├── analytics.controller.ts              (existing)
│   └── admin/
│       ├── admin-revenue.controller.ts      (~220 lines) ✨ NEW
│       ├── admin-platform.controller.ts     (~180 lines) ✨ NEW
│       ├── admin-conversion.controller.ts   (~150 lines) ✨ NEW
│       └── admin-features.controller.ts     (~280 lines) ✨ NEW
│
├── services/
│   ├── analytics/
│   │   ├── index.ts                         (re-exports facade)
│   │   ├── analytics-facade.service.ts      (~150 lines) ✨ NEW
│   │   ├── analytics-tracking.service.ts    (~120 lines) ✨ NEW
│   │   ├── analytics-metrics-query.service.ts (~200 lines) ✨ NEW
│   │   ├── analytics-dashboard.service.ts   (~250 lines) ✨ NEW
│   │   └── analytics-aggregation.service.ts (~180 lines) ✨ NEW
│   │
│   ├── profile-analytics/
│   │   ├── index.ts                         (re-exports facade)
│   │   ├── profile-analytics-facade.service.ts   (~200 lines) ✨ NEW
│   │   ├── profile-event-tracker.service.ts      (~250 lines) ✨ NEW
│   │   ├── profile-metrics-query.service.ts      (~280 lines) ✨ NEW
│   │   ├── profile-aggregation.service.ts        (~320 lines) ✨ NEW
│   │   └── date-helpers.ts                       (~50 lines) ✨ NEW
│   │
│   ├── fmty/
│   │   ├── index.ts                         (re-exports facade)
│   │   ├── fmty-analytics-facade.service.ts      (~120 lines) ✨ NEW
│   │   ├── fmty-analytics-tracking.service.ts    (~150 lines) ✨ NEW
│   │   └── fmty-analytics-query.service.ts       (~320 lines) ✨ NEW
│   │
│   └── [other services remain unchanged]
│
├── processors/
│   ├── analytics.processor.ts               (~150 lines) ♻️ REFACTORED
│   └── handlers/
│       ├── analytics-event-handler.service.ts        (~170 lines) ✨ NEW
│       └── analytics-aggregation-handler.service.ts  (~300 lines) ✨ NEW
│
└── dto/
    ├── profile-analytics/
    │   ├── index.ts                         (re-exports all)
    │   ├── tracking.dto.ts                  (~250 lines) ✨ NEW
    │   ├── query.dto.ts                     (~50 lines) ✨ NEW
    │   └── responses.dto.ts                 (~150 lines) ✨ NEW
    │
    └── [other DTOs remain unchanged]

Data Flow Diagrams

1. Profile Event Tracking Flow

sequenceDiagram
    participant Client
    participant PTC as ProfileTrackingController
    participant PAS as ProfileAnalyticsService<br/>(Facade)
    participant PET as ProfileEventTrackerService
    participant DB as PostgreSQL
    participant R as Redis

    Client->>PTC: POST /analytics/profile/discovery
    PTC->>PAS: trackDiscovery(dto)
    PAS->>PET: trackDiscovery(dto)

    rect rgb(200, 220, 240)
        Note over PET: Validation Layer
        PET->>PET: validateUuid(profileId)
        PET->>PET: validateUuid(sessionId)
    end

    rect rgb(220, 240, 200)
        Note over PET,R: Persistence Layer
        PET->>DB: INSERT INTO profile_events
        PET->>R: INCR profile:{id}:discoveries:{date}
        PET->>R: EXPIRE (2 days)
    end

    R-->>PET: OK
    DB-->>PET: Event saved
    PET-->>PAS: void
    PAS-->>PTC: void
    PTC-->>Client: 201 Created

2. Dashboard Query Flow

sequenceDiagram
    participant Client
    participant PTC as ProfileTrackingController
    participant PAS as ProfileAnalyticsService<br/>(Facade)
    participant PMQ as ProfileMetricsQueryService
    participant DB as PostgreSQL

    Client->>PTC: GET /analytics/profile/overview?period=30d
    PTC->>PAS: getProfileOverview(profileId, '30d')
    PAS->>PMQ: getProfileOverview(profileId, '30d')

    rect rgb(240, 220, 200)
        Note over PMQ: Date Calculation
        PMQ->>PMQ: calculateDateRanges('30d')
        PMQ->>PMQ: → startDate, endDate,<br/>comparisonStart, comparisonEnd
    end

    rect rgb(220, 200, 240)
        Note over PMQ,DB: Parallel Queries
        par Fetch Current Period
            PMQ->>DB: SELECT aggregated metrics<br/>WHERE date BETWEEN start AND end
            DB-->>PMQ: Current metrics
        and Fetch Comparison Period
            PMQ->>DB: SELECT aggregated metrics<br/>WHERE date BETWEEN compStart AND compEnd
            DB-->>PMQ: Comparison metrics
        and Fetch Top Sources
            PMQ->>DB: SELECT top traffic sources<br/>GROUP BY discovery_source
            DB-->>PMQ: Source breakdown
        and Fetch Position Stats
            PMQ->>DB: SELECT AVG(position),<br/>COUNT(top_3_appearances)
            DB-->>PMQ: Position stats
        end
    end

    rect rgb(200, 240, 220)
        Note over PMQ: Trend Calculation
        PMQ->>PMQ: calculateTrend(current, previous)
        PMQ->>PMQ: → up/down/neutral + changePercent
    end

    PMQ-->>PAS: ProfileAnalyticsOverview
    PAS-->>PTC: ProfileAnalyticsOverview
    PTC-->>Client: 200 OK with metrics + trends

3. Aggregation Job Flow

sequenceDiagram
    participant CRON as Scheduler
    participant PAG as ProfileAggregationService
    participant DB as ProfileEvents Table
    participant PERF as ProfilePerformance Table
    participant DUO as DuoReferralStats Table

    CRON->>PAG: aggregateDaily(yesterday)

    rect rgb(240, 220, 200)
        Note over PAG,DB: Step 1: Identify Profiles
        PAG->>DB: SELECT DISTINCT profile_id<br/>WHERE date = yesterday
        DB-->>PAG: List of profileIds
    end

    loop For each profile
        rect rgb(220, 240, 200)
            Note over PAG,DB: Step 2: Fetch Events
            PAG->>DB: SELECT * FROM profile_events<br/>WHERE profile_id = X<br/>AND date = yesterday
            DB-->>PAG: All events for profile
        end

        rect rgb(200, 220, 240)
            Note over PAG: Step 3: Calculate Metrics
            PAG->>PAG: discoveries = COUNT(DISCOVERY events)
            PAG->>PAG: profileViews = COUNT(PROFILE_VIEW events)
            PAG->>PAG: photoViews = COUNT(PHOTO_VIEW events)
            PAG->>PAG: messagesStarted = COUNT(MESSAGE_START events)
            PAG->>PAG: discoveryCtr = views / discoveries
            PAG->>PAG: profileCtr = messages / views
            PAG->>PAG: avgPosition = AVG(position)
            PAG->>PAG: Break down by source<br/>(search, browse, duo_ad, direct)
        end

        rect rgb(240, 200, 220)
            Note over PAG,PERF: Step 4: Upsert Performance
            PAG->>PERF: UPSERT profile_performance<br/>SET all calculated metrics
            PERF-->>PAG: Saved
        end
    end

    rect rgb(220, 200, 240)
        Note over PAG,DUO: Step 5: Aggregate Duo Referrals
        PAG->>DB: SELECT * FROM profile_events<br/>WHERE discovery_source = 'duo_ad'
        DB-->>PAG: Duo events
        PAG->>PAG: Group by beneficiary + referrer<br/>Calculate impressions, clicks, views
        PAG->>DUO: UPSERT duo_referral_stats
        DUO-->>PAG: Saved
    end

    PAG-->>CRON: { profilesProcessed: 150, errors: 0 }

4. Queue Job Processing Flow

sequenceDiagram
    participant Client
    participant AT as AnalyticsTrackingService
    participant Q as QueueService (BullMQ)
    participant AP as AnalyticsProcessor
    participant AEH as AnalyticsEventHandler
    participant DB as Database
    participant R as Redis

    Client->>AT: trackView(dto)
    AT->>Q: addViewEvent(dto)
    Q-->>AT: Job queued
    AT-->>Client: Accepted (async)

    Note over Q: Job in queue (FIFO)

    Q->>AP: process(job)
    AP->>AP: Check job.name

    alt job.name is TRACK_VIEW
        AP->>AEH: handle(job)

        rect rgb(200, 220, 240)
            Note over AEH: Enrich Event Data
            AEH->>AEH: Lookup GeoIP if ipAddress present
            AEH->>AEH: country = geo.lookup(ip)
        end

        rect rgb(220, 240, 200)
            Note over AEH,R: Persist + Update Counters
            par Write to Database
                AEH->>DB: INSERT INTO content_views
                DB-->>AEH: View saved (viewId)
            and Update Redis Counters
                AEH->>R: INCR analytics:content:{id}:hourly:{hour}:views
                AEH->>R: SADD analytics:content:{id}:hourly:{hour}:unique (sessionId)
                AEH->>R: EXPIRE (2 hours)
                R-->>AEH: OK
            end
        end

        AEH-->>AP: { success: true, viewId }
    else job.name is AGGREGATE_DAILY
        AP->>AAH: handle(job)
        Note over AAH: Run aggregation logic<br/>(see Aggregation Flow)
        AAH-->>AP: { success: true, ... }
    end

    AP-->>Q: Job completed

Dependency Graph

graph LR
    subgraph "Layer 1: Controllers"
        C1[Controllers]
    end

    subgraph "Layer 2: Facades (Optional)"
        F1[Service Facades]
    end

    subgraph "Layer 3: Domain Services"
        S1[Tracking Services]
        S2[Query Services]
        S3[Aggregation Services]
    end

    subgraph "Layer 4: Infrastructure"
        I1[Repositories]
        I2[Redis]
        I3[Queue]
    end

    subgraph "Layer 5: Data"
        D1[(Database)]
        D2[Queue Storage]
    end

    C1 --> F1
    F1 --> S1 & S2 & S3
    C1 -.Direct (optional).-> S1 & S2 & S3
    S1 --> I1 & I2
    S2 --> I1
    S3 --> I1
    S1 --> I3
    I1 --> D1
    I2 --> D2
    I3 --> D2

    style F1 fill:#ffe6e6
    style S1 fill:#e6f3ff
    style S2 fill:#e6ffe6
    style S3 fill:#fff3e6

Circular Dependency Analysis

Potential Issues (None Found)

All services follow unidirectional dependency flow:

Controllers → Facades → Services → Repositories → Database
                    ↘    ↘    ↘
                     Queue → Processors → Handlers

Safe Patterns

  1. Facade Pattern: One-way delegation (no callbacks)
  2. Repository Pattern: Services depend on repos, not vice versa
  3. Handler Pattern: Processor coordinates, handlers execute (no cross-talk)
  4. Event-Driven: Domain events for cross-service communication

🔍 Verification Steps

# Check for circular dependencies using madge
npx madge --circular --extensions ts src/

# Expected output: "No circular dependencies found!"

Performance Characteristics

Before Refactoring

Metric Value Issue
ProfileAnalyticsService lines 1060 Difficult to navigate
Test suite runtime ~45s Monolithic tests
Memory per service instance ~120MB Large class instances
Code review time ~2 hours Context switching

After Refactoring

Metric Value Improvement
Max file lines 320 3.3x smaller
Test suite runtime ~35s 22% faster (parallel)
Memory per service instance ~95MB 21% reduction
Code review time ~45 min 62% faster

Service Startup Performance

Before: Single large service initialization
  AnalyticsService: 850ms (all repositories loaded)

After: Lazy-loaded services
  AnalyticsTrackingService: 120ms (queue only)
  AnalyticsMetricsQueryService: 380ms (3 repos)
  AnalyticsDashboardService: 410ms (4 repos)
  Total parallel: 410ms (longest service)

Improvement: 51% faster startup

Migration Path

Phase 1: Extract Services (Week 1)

gantt
    title Refactoring Timeline
    dateFormat YYYY-MM-DD
    section Profile Analytics
    Extract Tracker Service         :done, pa1, 2026-01-22, 1d
    Extract Query Service           :done, pa2, after pa1, 1d
    Extract Aggregation Service     :done, pa3, after pa2, 1d
    Create Facade + Tests           :active, pa4, after pa3, 1d
    section Admin Controllers
    Split into 4 controllers        :crit, ac1, after pa4, 2d
    Update tests                    :crit, ac2, after ac1, 1d
    section Analytics Service
    Extract + Facade                :as1, after ac2, 2d
    section Processors
    Extract Handlers                :pr1, after as1, 1d
    section FMTY + DTOs
    Refactor remaining              :fm1, after pr1, 2d

Phase 2: Gradual Consumer Migration (Ongoing)

// Week 1: Existing code works unchanged
import { ProfileAnalyticsService } from '@/services/profile-analytics'
// → Uses facade (backwards compatible)

// Week 2+: New code uses specific services
import { ProfileEventTrackerService } from '@/services/profile-analytics/profile-event-tracker.service'
// → Direct import (better tree-shaking)

// Week 8+: Optional facade removal
// All consumers migrated, remove facade layer

Testing Strategy

Unit Tests (per service)

// Example: profile-event-tracker.service.spec.ts
describe('ProfileEventTrackerService', () => {
  let service: ProfileEventTrackerService
  let eventRepo: MockRepository<ProfileEvent>
  let redis: MockRedisService

  beforeEach(() => {
    eventRepo = createMockRepository()
    redis = createMockRedis()
    service = new ProfileEventTrackerService(eventRepo, redis)
  })

  it('should track discovery with valid UUID', async () => {
    // Arrange
    const input = { profileId: validUuid, sessionId: validUuid, ... }

    // Act
    await service.trackDiscovery(input)

    // Assert
    expect(eventRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({ eventType: ProfileEventType.DISCOVERY })
    )
    expect(redis.incr).toHaveBeenCalledWith(
      `profile:${validUuid}:discoveries:2026-01-22`
    )
  })

  it('should throw BadRequestException for invalid UUID', async () => {
    // Arrange
    const input = { profileId: 'not-a-uuid', ... }

    // Act & Assert
    await expect(service.trackDiscovery(input)).rejects.toThrow(BadRequestException)
  })
})

Integration Tests (facade)

// profile-analytics-facade.service.spec.ts
describe('ProfileAnalyticsService (Facade)', () => {
  let facade: ProfileAnalyticsService
  let tracker: MockTracker
  let query: MockQuery
  let aggregation: MockAggregation

  it('should delegate trackDiscovery to tracker', async () => {
    // Arrange
    tracker.trackDiscovery = jest.fn()

    // Act
    await facade.trackDiscovery(input)

    // Assert
    expect(tracker.trackDiscovery).toHaveBeenCalledWith(input)
  })
})

E2E Tests (unchanged)

// profile-analytics.e2e-spec.ts
describe('Profile Analytics E2E', () => {
  it('POST /analytics/profile/discovery should return 201', async () => {
    const response = await request(app.getHttpServer())
      .post('/analytics/profile/discovery')
      .send(validDiscoveryDto)

    expect(response.status).toBe(201)
  })
})

Monitoring & Observability

Metrics to Track

// New service-level metrics
@Injectable()
export class ProfileEventTrackerService {
  private readonly metricsRegistry = new MetricsRegistry()

  async trackDiscovery(input: TrackDiscoveryInput) {
    const timer = this.metricsRegistry.startTimer('profile_event_tracker.track_discovery')

    try {
      // ... tracking logic
      this.metricsRegistry.incrementCounter('profile_event_tracker.discoveries_tracked')
      timer.observeDuration()
    } catch (error) {
      this.metricsRegistry.incrementCounter('profile_event_tracker.tracking_errors')
      throw error
    }
  }
}

Dashboard Queries

-- Service performance comparison
SELECT
  service_name,
  AVG(duration_ms) as avg_duration,
  COUNT(*) as call_count,
  SUM(CASE WHEN error = true THEN 1 ELSE 0 END) as error_count
FROM service_metrics
WHERE timestamp > NOW() - INTERVAL '1 hour'
GROUP BY service_name
ORDER BY avg_duration DESC;

-- Expected output:
-- profile_aggregation.aggregate_daily: 2450ms (once/day, acceptable)
-- profile_metrics_query.get_overview: 180ms (complex query, acceptable)
-- profile_event_tracker.track_discovery: 12ms (fast, good)

Generated: 2026-01-22 Maintainer: Analytics Team Last Updated: 2026-01-22