This document covers the implementation of four major features requested in GitHub issues #546, #548, #545, and #554.
- Event Tracking System (#546)
- A/B Testing Framework (#548)
- Search Suggestions and Autocomplete (#545)
- Subscription Management System (#554)
A comprehensive event tracking system that captures user actions for analytics, with built-in validation, batching, and performance optimization.
- Event SDK: Simple client API for tracking common events (signup, login, course_view, purchase, etc.)
- Event Validation: Schema-based validation for event types with custom rules
- Event Batching: Automatic batching and flushing for performance (configurable batch size & interval)
- Event Persistence: Events stored in PostgreSQL with indexed queries
- Prometheus Integration: All events tracked as metrics for monitoring
- Analytics Dashboard: Summary endpoints for event analysis
src/analytics/
├── entities/
│ └── event.entity.ts (NEW) - AnalyticsEvent entity with EventType enum
├── services/
│ ├── event-batching.service.ts (NEW) - Batch processing with auto-flush
│ └── event-validation.service.ts (NEW) - Schema-based validation
├── sdk/
│ └── event-tracking.sdk.ts (NEW) - High-level tracking SDK
├── analytics.service.ts (MODIFIED) - Enhanced with batching & validation
├── analytics.controller.ts (MODIFIED) - New API endpoints
└── analytics.module.ts (MODIFIED) - Register new services
CREATE TABLE analytics_events (
id UUID PRIMARY KEY,
eventType VARCHAR(64),
category VARCHAR(64),
action VARCHAR(64),
label VARCHAR(128),
value DECIMAL,
properties JSONB,
sessionId VARCHAR,
fingerprintId VARCHAR,
userId VARCHAR,
ipAddress VARCHAR,
userAgent VARCHAR,
timestamp TIMESTAMP,
createdAt TIMESTAMP,
INDEX: [userId, eventType, createdAt],
INDEX: [eventType, createdAt],
INDEX: [timestamp]
);POST /analytics/events
Authorization: Bearer <token>
Content-Type: application/json
{
"eventType": "signup|login|course_view|purchase|custom",
"category": "user|course|purchase",
"action": "signup|login|view",
"label": "optional_label",
"value": 49.99,
"properties": { "key": "value" }
}
Response: { "success": true }GET /analytics/events?eventType=purchase&category=purchase&limit=50&offset=0
Authorization: Bearer <token>
Response: { "events": [...], "total": 100 }GET /analytics/summary?startDate=2026-05-01&endDate=2026-06-01
Response: {
"totalEvents": 10000,
"eventsByType": { "signup": 500, "login": 5000, "purchase": 1000 },
"eventsByCategory": { "user": 5500, "course": 3500, "purchase": 1000 },
"topActions": [{ "action": "login", "count": 5000 }]
}Environment variables:
EVENT_BATCH_SIZE=100 # Events before flush
EVENT_FLUSH_INTERVAL_MS=5000 # Auto-flush intervalUsing the Event SDK:
import { EventTrackingSDK } from '@/analytics/sdk/event-tracking.sdk';
@Injectable()
export class UserService {
constructor(private eventSDK: EventTrackingSDK) {}
async signup(user: User): Promise<void> {
// ... signup logic ...
await this.eventSDK.trackSignup(user.id, { source: 'organic' });
}
async purchase(userId: string, courseId: string, amount: number): Promise<void> {
// ... payment logic ...
await this.eventSDK.trackPurchase(userId, courseId, amount, { paymentMethod: 'stripe' });
}
}| Event Type | Required Fields | Validation Rules |
|---|---|---|
| SIGNUP | userId, category, action | Valid UUID for userId |
| LOGIN | userId, category, action | Valid UUID for userId |
| COURSE_VIEW | userId, properties.courseId | courseId is valid UUID |
| PURCHASE | userId, value, properties.courseId | value > 0, valid UUIDs |
| CUSTOM | category, action | No strict validation |
Run E2E tests:
npm run test:e2e -- test/event-tracking.e2e-spec.tsA production-ready A/B testing framework with statistical analysis, experiment templates, and auto-stop capabilities based on statistical significance.
- Experiment Templates: Pre-configured templates (standard, quick, high-confidence)
- Simplified Setup: Easy experiment creation with templates
- Statistical Analysis: P-value, confidence level, uplift calculation
- Auto-Stop: Automatically stop experiments when significance threshold reached
- Results Dashboard: Comprehensive results visualization
- Variant Assignment: User-to-variant assignment logic
- Event Emission: Integration with event system for downstream processing
src/ab-testing/
├── ab-testing.service.ts (MODIFIED) - Enhanced with templates, analysis, auto-stop
└── ab-testing.controller.ts (MODIFIED) - New endpoints for templates and analysis
Standard Template (95% confidence)
{
"name": "Standard A/B Test",
"trafficAllocation": 50,
"confidenceLevel": 0.95,
"minimumSampleSize": 1000,
"autoStopOnSignificance": true,
"significanceThreshold": 0.95
}Quick Template (90% confidence, rapid iteration)
{
"name": "Quick Test",
"trafficAllocation": 100,
"confidenceLevel": 0.90,
"minimumSampleSize": 200,
"autoStopOnSignificance": true,
"significanceThreshold": 0.90
}High Confidence Template (99% confidence, critical decisions)
{
"name": "High Confidence Test",
"trafficAllocation": 50,
"confidenceLevel": 0.99,
"minimumSampleSize": 5000,
"autoStopOnSignificance": true,
"significanceThreshold": 0.99
}GET /ab-testing/templates
Response: [
{ "name": "Standard A/B Test", "trafficAllocation": 50, ... },
{ "name": "Quick Test", "trafficAllocation": 100, ... },
{ "name": "High Confidence Test", "trafficAllocation": 50, ... }
]POST /ab-testing/experiments
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"name": "Homepage Button Color Test",
"description": "Testing blue vs green CTA button",
"type": "feature_flag",
"hypothesis": "Green button will increase conversions by 15%",
"templateName": "standard", // Uses standard template settings
"variants": [
{
"name": "Control (Blue)",
"description": "Original blue button",
"configuration": { "color": "blue" },
"isControl": true
},
{
"name": "Treatment (Green)",
"configuration": { "color": "green" },
"isControl": false
}
],
"metrics": [
{
"name": "Click Through Rate",
"type": "conversion",
"isPrimary": true
}
]
}
Response: {
"id": "exp-123",
"status": "draft",
"properties": { "templateUsed": "standard" }
}POST /ab-testing/experiments/{id}/start
Authorization: Bearer <admin-token>
Response: { "id": "exp-123", "status": "running" }POST /ab-testing/experiments/{id}/analyze
Authorization: Bearer <admin-token>
Response: {
"results": [
{
"variantId": "var-1",
"sampleSize": 2500,
"conversionRate": 0.085,
"confidence": 0.97,
"pValue": 0.03,
"isSignificant": true,
"uplift": 0.15,
"upliftCI": { "lower": 0.08, "upper": 0.22 }
}
],
"shouldStop": true,
"reason": "Statistical significance reached"
}GET /ab-testing/experiments/{id}/dashboard
Authorization: Bearer <admin-token>
Response: {
"experiment": { ... },
"variantResults": [ ... ],
"summary": {
"winner": "var-2",
"confidence": 0.96,
"estimatedUplift": 0.12,
"sampleSizeReached": true
}
}The framework calculates:
- P-value: Statistical significance of the difference
- Confidence Level: 1 - p-value (e.g., 95% confidence means 5% p-value threshold)
- Uplift: Relative improvement of treatment vs control
- Uplift CI: Confidence interval for uplift estimate
- Sample Size: Number of observations per variant
Run E2E tests:
npm run test:e2e -- test/ab-testing.e2e-spec.tsAn autocomplete API providing real-time search suggestions with multi-source aggregation (courses, categories, trending searches) and intelligent caching.
- Multi-Source Suggestions: Combines course titles, categories, trending searches
- Type-Aware Results: Different suggestion types (course, category, trending)
- Smart Caching: 5-minute cache with cache stats endpoint
- Performance Optimized: Response time < 500ms target
- Deduplication: Automatic removal of duplicate suggestions
- Database Integration: Searches against PostgreSQL for courses
src/search/
├── search.service.ts (MODIFIED) - Autocomplete & multi-source search
└── search.controller.ts (EXISTING) - Already implemented API routes
-- Assumes courses table exists with indexed columns
ALTER TABLE courses ADD INDEX idx_title_search (title);GET /search/autocomplete?q=java
Response: [
{
"title": "JavaScript Fundamentals",
"type": "course",
"metadata": { "courseId": "course-123", "category": "programming" }
},
{
"title": "Java Advanced",
"type": "course",
"metadata": { "courseId": "course-456", "category": "programming" }
},
{
"title": "programming",
"type": "category",
"metadata": { "category": "programming" }
},
{
"title": "JavaScript",
"type": "trending",
"metadata": { "popular": true }
}
]GET /search?q=javascript&filters={...}&sort=relevance&page=1&limit=20
Response: {
"results": [...courses...],
"total": 150,
"page": 1,
"limit": 20,
"query": "javascript",
"filters": {}
}GET /search/filters
Response: {
"categories": ["programming", "web-development", "design", ...],
"levels": ["beginner", "intermediate", "advanced"],
"languages": ["en", "es", "fr", "de", "zh"],
"priceRanges": [
{ "label": "Free", "lte": 0 },
{ "label": "Under $50", "gte": 0, "lte": 50 }
]
}- Query Validation: Minimum 2 characters for suggestions
- Cache Lookup: Check 5-minute TTL cache
- Database Search: Query courses table for matches
- Category Aggregation: Add matching categories
- Trending Addition: Add popular trending searches
- Deduplication: Remove duplicates by title:type
- Limiting: Return top 10 results max
- Cache Store: Store results for future queries
- Empty cache: ~100-200ms (database queries)
- Cached result: ~10-20ms (in-memory)
- Response size: ~5KB typical (10 suggestions)
- Cache TTL: 300 seconds (5 minutes)
Run E2E tests:
npm run test:e2e -- test/search-autocomplete.e2e-spec.tsA complete subscription lifecycle management system with pause/resume, upgrade/downgrade with proration, and retry logic for failed renewals.
- Pause/Resume: Temporarily suspend subscriptions with optional resume date
- Upgrade/Downgrade: Plan changes with automatic proration calculation
- Renewal Management: Automatic renewal with exponential backoff retry
- Prorated Pricing: Accurate calculation of credits/charges for plan changes
- Failed Payment Handling: Mark subscriptions as past_due after max retries
- Event Emission: Integration events for downstream processing
src/payments/
├── entities/subscription.entity.ts (MODIFIED) - Added properties JSONB column
├── subscriptions/
│ ├── subscriptions.service.ts (MODIFIED) - Complete implementation
│ ├── subscriptions.controller.ts (NEW) - REST API endpoints
│ └── dto/subscription-action.dto.ts (NEW) - Request/response DTOs
ALTER TABLE subscriptions ADD COLUMN properties JSONB DEFAULT '{}'::jsonb;
-- Properties stored:
-- { isPaused, pausedAt, pauseReason, resumeAt, resumedAt,
-- upgradedFrom, downgradedFrom, proratedAmount, proratedCredit,
-- lastRenewalAttempt, renewalAttempts, failedRenewalAttempts }GET /subscriptions/me
Authorization: Bearer <token>
Response: {
"id": "sub-123",
"status": "active",
"amount": 29.99,
"interval": "monthly",
"currentPeriodStart": "2026-06-01T00:00:00Z",
"currentPeriodEnd": "2026-07-01T00:00:00Z"
}PATCH /subscriptions/{id}/pause
Authorization: Bearer <token>
Content-Type: application/json
{
"reason": "Need a break from course",
"resumeAt": "2026-07-01T00:00:00Z" // Optional
}
Response: {
"id": "sub-123",
"status": "active",
"properties": {
"isPaused": true,
"pausedAt": "2026-06-10T10:00:00Z",
"pauseReason": "Need a break from course",
"resumeAt": "2026-07-01T00:00:00Z"
}
}PATCH /subscriptions/{id}/resume
Authorization: Bearer <token>
Content-Type: application/json
{
"reason": "Ready to continue"
}
Response: {
"id": "sub-123",
"status": "active",
"properties": {
"isPaused": false,
"resumedAt": "2026-06-20T10:00:00Z"
}
}POST /subscriptions/{id}/upgrade
Authorization: Bearer <token>
Content-Type: application/json
{
"planId": "plan-pro",
"billingCycle": "monthly"
}
Response: {
"id": "sub-123",
"amount": 49.99, // New plan price
"properties": {
"upgradedFrom": { "planId": "plan-basic", "amount": 29.99 },
"upgradedAt": "2026-06-15T10:00:00Z",
"proratedAmount": 13.70, // Charge for upgrade
"proratedCredit": 0,
"proratedCharge": 49.99
}
}POST /subscriptions/{id}/downgrade
Authorization: Bearer <token>
Content-Type: application/json
{
"planId": "plan-basic",
"prorationType": "credit" // or "next_billing_cycle", "immediate"
}
Response: {
"id": "sub-123",
"amount": 29.99, // New plan price
"properties": {
"downgradedFrom": { "planId": "plan-pro", "amount": 49.99 },
"downgradedAt": "2026-06-15T10:00:00Z",
"prorationType": "credit",
"proratedCredit": 13.70 // Credit to account
}
}For plan changes mid-billing period:
const daysRemaining = (periodEnd - now) / (24 * 60 * 60 * 1000);
const totalDaysInPeriod = 30; // or 7, 90, 365 depending on interval
// Upgrade calculation
const oldCharge = oldAmount * (daysRemaining / totalDaysInPeriod);
const newCharge = newAmount * (daysRemaining / totalDaysInPeriod);
const proratedAmount = newCharge - oldCharge; // Additional charge
// Downgrade calculation
const credit = oldCharge - newCharge; // Refund/credit- Scheduled Renewal: Cron job or background worker checks for upcoming renewals
- Attempt Payment: Try to charge the customer
- Retry Logic:
- First failure: Immediate retry
- Second failure: Retry after 2 seconds (exponential backoff)
- Third failure: Retry after 4 seconds
- After max retries: Mark as PAST_DUE, emit event
- Auto-Retry: Can schedule another renewal attempt later
// Process renewal with retries
const success = await subscriptionService.processRenewal(subscriptionId, maxRetries=3);
if (!success) {
// Mark subscription as past_due
await subscriptionService.scheduleRenewalRetry(subscriptionId, delayMs=300000);
}Events emitted by subscription service:
'subscription.paused' -> { subscriptionId, userId, resumeAt, reason }
'subscription.resumed' -> { subscriptionId, userId, reason }
'subscription.upgraded' -> { subscriptionId, userId, oldAmount, newAmount, proratedAmount }
'subscription.downgraded' -> { subscriptionId, userId, oldAmount, newAmount, proratedCredit }
'subscription.renewal_attempt' -> { subscriptionId, userId, amount, attempt, maxRetries }
'subscription.renewed' -> { subscriptionId, userId }
'subscription.renewal_failed' -> { subscriptionId, userId, attempts }Run E2E tests:
npm run test:e2e -- test/subscription-management.e2e-spec.tsAll modules are registered in app.module.ts:
// Analytics
import { AnalyticsModule } from './analytics/analytics.module';
// Payments
import { PaymentsModule } from './payments/payments.module';
// Search
import { SearchModule } from './search/search.module';
// A/B Testing
import { ABTestingModule } from './ab-testing/ab-testing.module';
@Module({
imports: [
// ... other modules
AnalyticsModule,
PaymentsModule,
SearchModule,
ABTestingModule,
],
})
export class AppModule {}All protected endpoints require:
Authorization: Bearer <JWT_TOKEN>Roles required:
- Event tracking: User (public)
- A/B Testing: ADMIN
- Search: Public
- Subscriptions: ADMIN, TEACHER
All endpoints return standardized error responses:
{
"statusCode": 400,
"message": "Event validation failed: Required field missing: value",
"error": "Bad Request"
}- Batching: Reduces database writes by ~90%
- Indexing: Fast queries on userId, eventType, timestamp
- Retention: Consider archiving old events (> 1 year) to separate table
- Caching: Results cached to avoid recalculation
- User Assignment: Use consistent hashing for stable assignments
- Metrics: Aggregate metrics asynchronously
- Caching: 5-minute cache on autocomplete queries
- Database: Use full-text search indexes on course titles
- Elasticsearch: Consider for production scale (100k+ courses)
- Renewal Job: Run every hour to catch renewals
- Batch Processing: Process renewals in batches (100 at a time)
- Retry Queue: Use Bull queue for reliable retry processing
// Event tracking metrics
feature_events_total{category,action,eventType} counter
// A/B Testing
experiments_active gauge
experiment_results_uplift histogram
experiment_auto_stopped_total counter
// Search
search_queries_total counter
autocomplete_response_time_ms histogram
autocomplete_cache_hit_rate gauge
// Subscriptions
subscriptions_active gauge
renewal_attempts_total counter
renewal_failures_total counter// Event batching
'Event batch flushed: 100 events'
'Autocomplete cache cleared'
'Renewal attempt 1/3 for subscription-123'
'Subscription upgraded: $29.99 -> $49.99 (prorated: $13.70)'- Check
EVENT_BATCH_SIZEandEVENT_FLUSH_INTERVAL_MSenv vars - Verify database connection is healthy
- Check logs for "Failed to flush batch" errors
- Verify
autoStopOnSignificanceis true in experiment config - Check if
minimumSampleSizehas been reached - Verify
significanceThresholdis set correctly
- Check if query length < 2 characters (returns empty)
- Look for cache misses in stats:
GET /search/autocomplete/stats - Consider adding database indexes on
courses.title
- Check payment processor connectivity
- Verify customer payment method is valid
- Check
properties.failedRenewalAttemptscount - Review renewal event emissions in logs
- Event Tracking: Real-time streaming to Kafka/Pub-Sub for real-time analytics
- A/B Testing: Bayesian analysis for faster convergence
- Search: Elasticsearch integration for relevance ranking
- Subscriptions: Webhook integration with Stripe/Paddle for payment updates
- TypeORM: https://typeorm.io
- NestJS: https://docs.nestjs.com
- Bull Queues: https://github.com/OptimalBits/bull
- Statistical Analysis: https://en.wikipedia.org/wiki/A/B_testing