Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 3 additions & 2 deletions nest-cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
"deleteOutDir": false,
"builder": "tsc"
}
}
}
10,728 changes: 3,043 additions & 7,685 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
"license": "UNLICENSED",
"scripts": {
"license:scan": "node scripts/scan-licenses.js",
"build": "nest build",
"build": "tsc -p tsconfig.build.json",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"start:prod": "node dist/main.js",
"start:simple": "node simple-server.js",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"lint:ci": "eslint \"{src,apps,libs,test}/**/*.ts\" --max-warnings 0",
"lint:typed": "eslint \"{src,apps,libs,test}/**/*.ts\" --parser-options=project:tsconfig.json --max-warnings 0",
Expand Down Expand Up @@ -72,15 +73,15 @@
"@nestjs/axios": "^4.0.1",
"@nestjs/bull": "^11.0.2",
"@nestjs/cache-manager": "^3.0.1",
"@nestjs/common": "^10.4.22",
"@nestjs/common": "10.4.22",
"@nestjs/config": "^4.0.3",
"@nestjs/core": "^10.4.22",
"@nestjs/core": "10.4.22",
"@nestjs/elasticsearch": "^11.1.0",
"@nestjs/event-emitter": "^3.1.0",
"@nestjs/graphql": "^12.2.2",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^10.4.18",
"@nestjs/platform-express": "10.4.22",
"@nestjs/platform-socket.io": "^10.4.22",
"@nestjs/schedule": "^6.1.0",
"@nestjs/swagger": "^7.4.2",
Expand Down Expand Up @@ -131,7 +132,6 @@
"pdf-parse": "^1.1.1",
"pg": "^8.17.2",
"prom-client": "^15.1.3",
"reacts-cli": "^1.0.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"sharp": "^0.34.3",
Expand All @@ -148,7 +148,7 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@openapitools/openapi-generator-cli": "^2.32.0",
"@openapitools/openapi-generator-cli": "^2.31.0",
"@types/babel__core": "^7.20.5",
"@types/babel__generator": "^7.27.0",
"@types/babel__template": "^7.4.4",
Expand Down Expand Up @@ -188,7 +188,7 @@
"ts-loader": "^9.5.7",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^6.0.3"
"typescript": "^5.4.5"
},
"engines": {
"node": ">=18.0.0",
Expand Down
41 changes: 41 additions & 0 deletions simple-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const express = require('express');
const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Basic health check endpoint
app.get('/', (req, res) => {
res.json({
message: 'TeachLink API is running',
timestamp: new Date().toISOString(),
status: 'OK'
});
});

// Basic API info endpoint
app.get('/api', (req, res) => {
res.json({
name: 'TeachLink API',
version: '1.0.0',
status: 'Running',
endpoints: ['/', '/api', '/health']
});
});

// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
console.log(`πŸš€ Server is running on port ${PORT}`);
console.log(`πŸ“š API available at http://localhost:${PORT}/api`);
console.log(`πŸ₯ Health check at http://localhost:${PORT}/health`);
});
133 changes: 32 additions & 101 deletions src/ab-testing/ab-testing.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ export interface ICreateMetricDto {
*/
@Injectable()
export class ABTestingService {
private readonly logger = new Logger(ABTestingService.name);
constructor(
private readonly logger = new Logger(ABTestingService.name);

constructor(
@InjectRepository(Experiment)
private experimentRepository: Repository<Experiment>,
@InjectRepository(IExperimentVariant)
Expand Down Expand Up @@ -70,10 +71,8 @@ export class ABTestingService {
experiment.exclusionCriteria = createExperimentDto.exclusionCriteria;
experiment.status = ExperimentStatus.DRAFT;

// Save the experiment first
const savedExperiment = await this.experimentRepository.save(experiment);

// Create variants
const variants = createExperimentDto.variants.map((variantDto) => {
const variant = new IExperimentVariant();
variant.name = variantDto.name;
Expand All @@ -90,19 +89,13 @@ export class ABTestingService {
return savedExperiment;
}

/**
* Gets all experiments
*/
async getAllExperiments(): Promise<Experiment[]> {
return await this.experimentRepository.find({
relations: ['variants', 'metrics'],
order: { createdAt: 'DESC' },
});
}

/**
* Gets experiment by ID
*/
async getExperimentById(id: string): Promise<Experiment> {
const experiment = await this.experimentRepository.findOne({
where: { id },
Expand All @@ -112,63 +105,29 @@ export class ABTestingService {
if (!experiment) {
throw new Error(`Experiment with ID ${id} not found`);
}
/**
* Gets all experiments
*/
async getAllExperiments(): Promise<Experiment[]> {
return await this.experimentRepository.find({
relations: ['variants', 'metrics'],
order: { createdAt: 'DESC' },
});
return experiment;
}

async startExperiment(id: string): Promise<Experiment> {
this.logger.log(`Starting experiment: ${id}`);
const experiment = await this.getExperimentById(id);
if (experiment.status !== ExperimentStatus.DRAFT) {
throw new Error('Only draft experiments can be started');
}
/**
* Gets experiment by ID
*/
async getExperimentById(id: string): Promise<Experiment> {
const experiment = await this.experimentRepository.findOne({
where: { id },
relations: ['variants', 'metrics', 'variants.metrics'],
});
if (!experiment) {
throw new Error(`Experiment with ID ${id} not found`);
}
return experiment;
if (!experiment.variants || experiment.variants.length < 2) {
throw new Error('Experiment must have at least 2 variants');
}
/**
* Starts an experiment
*/
async startExperiment(id: string): Promise<Experiment> {
this.logger.log(`Starting experiment: ${id}`);
const experiment = await this.getExperimentById(id);
if (experiment.status !== ExperimentStatus.DRAFT) {
throw new Error('Only draft experiments can be started');
}
if (!experiment.variants || experiment.variants.length < 2) {
throw new Error('Experiment must have at least 2 variants');
}
// Validate that there's exactly one control variant
const controlVariants = experiment.variants.filter((v) => v.isControl);
if (controlVariants.length !== 1) {
throw new Error('Experiment must have exactly one control variant');
}
experiment.status = ExperimentStatus.RUNNING;
experiment.startDate = new Date();
const updatedExperiment = await this.experimentRepository.save(experiment);
this.logger.log(`Experiment started: ${updatedExperiment.name}`);
return updatedExperiment;
const controlVariants = experiment.variants.filter((v) => v.isControl);
if (controlVariants.length !== 1) {
throw new Error('Experiment must have exactly one control variant');
}

experiment.status = ExperimentStatus.RUNNING;
experiment.startDate = new Date();

const updatedExperiment = await this.experimentRepository.save(experiment);
this.logger.log(`Experiment started: ${updatedExperiment.name}`);
return updatedExperiment;
}

/**
* Stops an experiment
*/
async stopExperiment(id: string): Promise<Experiment> {
this.logger.log(`Stopping experiment: ${id}`);

Expand All @@ -181,62 +140,34 @@ export class ABTestingService {
return updatedExperiment;
}

/**
* Gets active experiments for a user
*/
async getActiveExperimentsForUser(_userId: string): Promise<Experiment[]> {
return await this.experimentRepository.find({
where: {
status: ExperimentStatus.RUNNING,
startDate: new Date(),
},
where: { status: ExperimentStatus.RUNNING },
relations: ['variants'],
});
}

/**
* Assigns a user to a variant
*/
async assignUserToVariant(experimentId: string, userId: string): Promise<IExperimentVariant> {
const experiment = await this.getExperimentById(experimentId);

if (experiment.status !== ExperimentStatus.RUNNING) {
throw new Error('Experiment is not running');
}
/**
* Gets active experiments for a user
*/
async getActiveExperimentsForUser(_userId: string): Promise<Experiment[]> {
return await this.experimentRepository.find({
where: {
status: ExperimentStatus.RUNNING,
startDate: new Date(),
},
relations: ['variants'],
});
if (!experiment.variants?.length) {
throw new Error('Experiment has no variants');
}
/**
* Assigns a user to a variant
*/
async assignUserToVariant(experimentId: string, userId: string): Promise<ExperimentVariant> {
const experiment = await this.getExperimentById(experimentId);
if (experiment.status !== ExperimentStatus.RUNNING) {
throw new Error('Experiment is not running');
}
// Simple hash-based assignment for consistent user-to-variant mapping
const variantIndex = this.hashUserIdToVariant(userId, experiment.variants.length);
return experiment.variants[variantIndex];
}
/**
* Hashes user ID to determine variant assignment
*/
private hashUserIdToVariant(userId: string, variantCount: number): number {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
const char = userId.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash) % variantCount;

const variantIndex = this.hashUserIdToVariant(userId, experiment.variants.length);
return experiment.variants[variantIndex];
}

private hashUserIdToVariant(userId: string, variantCount: number): number {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
const char = userId.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash) % variantCount;
}
}
Loading
Loading