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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// src/engagement-analytics/controllers/engagement-analytics.controller.ts
import { Controller, Get, Post, Body, Query, Param, UseGuards, HttpException, HttpStatus, Logger, CacheTTL } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiQuery, ApiParam } from '@nestjs/swagger';
import { EngagementAnalyticsService } from '../services/engagement-analytics.service';
import { CreateEngagementDto } from '../dto/create-engagement.dto';
import { EngagementQueryDto, TimeFrame } from '../dto/engagement-query.dto';
import { Engagement } from '../entities/engagement.entity';
import { EngagementSummary, EngagementTimeSeries } from '../interfaces/engagement-analytics.interface';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';

@ApiTags('engagement-analytics')
@Controller('engagement-analytics')
export class EngagementAnalyticsController {
private readonly logger = new Logger(EngagementAnalyticsController.name);

constructor(private readonly analyticsService: EngagementAnalyticsService) {}

@Post()
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Record a new engagement' })
@ApiResponse({ status: 201, description: 'Engagement recorded successfully', type: Engagement })
@ApiBody({ type: CreateEngagementDto })
async recordEngagement(@Body() createDto: CreateEngagementDto): Promise<Engagement> {
try {
return await this.analyticsService.recordEngagement(createDto);
} catch (error) {
this.logger.error(`Failed to record engagement: ${error.message}`, error.stack);
throw new HttpException('Failed to record engagement', HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@Get()
@CacheTTL(300) // 5 minute cache
@ApiOperation({ summary: 'Get engagement analytics' })
@ApiResponse({ status: 200, description: 'Analytics retrieved successfully' })
@ApiQuery({ name: 'contentId', required: false })
@ApiQuery({ name: 'timeFrame', enum: TimeFrame, required: false })
async getAnalytics(@Query() queryDto: EngagementQueryDto): Promise<EngagementSummary> {
try {
return await this.analyticsService.getEngagementAnalytics(queryDto);
} catch (error) {
this.logger.error(`Failed to get analytics: ${error.message}`, error.stack);
throw new HttpException('Failed to retrieve analytics', HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@Get('time-series')
@CacheTTL(300) // 5 minute cache
@ApiOperation({ summary: 'Get engagement time series data' })
@ApiResponse({ status: 200, description: 'Time series data retrieved successfully' })
async getTimeSeriesData(@Query() queryDto: EngagementQueryDto): Promise<EngagementTimeSeries[]> {
try {
return await this.analyticsService.getTimeSeriesData(queryDto);
} catch (error) {
this.logger.error(`Failed to get time series data: ${error.message}`, error.stack);
throw new HttpException('Failed to retrieve time series data', HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@Get('content/:contentId')
@CacheTTL(300) // 5 minute cache
@ApiOperation({ summary: 'Get analytics for a specific content item' })
@ApiResponse({ status: 200, description: 'Content analytics retrieved successfully' })
@ApiParam({ name: 'contentId', required: true })
async getContentAnalytics(
@Param('contentId') contentId: string,
@Query() queryDto: EngagementQueryDto,
): Promise<EngagementSummary> {
try {
return await this.analyticsService.getEngagementAnalytics({
...queryDto,
contentId,
});
} catch (error) {
this.logger.error(`Failed to get content analytics: ${error.message}`, error.stack);
throw new HttpException('Failed to retrieve content analytics', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// src/engagement-analytics/controllers/engagement-reporting.controller.ts
import { Controller, Get, Post, Body, Query, Res, UseGuards, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
import { EngagementReportingService } from '../services/engagement-reporting.service';
import { EngagementReportDto, ReportFormat } from '../dto/engagement-report.dto';
import { Response } from 'express';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';

@ApiTags('engagement-reporting')
@Controller('engagement-reporting')
export class EngagementReportingController {
private readonly logger = new Logger(EngagementReportingController.name);

constructor(private readonly reportingService: EngagementReportingService) {}

@Post('generate')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Generate an engagement report' })
@ApiResponse({ status: 200, description: 'Report generated successfully' })
@ApiBody({ type: EngagementReportDto })
async generateReport(
@Body() reportDto: EngagementReportDto,
@Res() res: Response,
): Promise<void> {
try {
const report = await this.reportingService.generateReport(reportDto);

// Set appropriate headers based on report format
switch (reportDto.format) {
case ReportFormat.CSV:
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="engagement_report.csv"`);
break;
case ReportFormat.PDF:
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="engagement_report.pdf"`);
break;
default:
res.setHeader('Content-Type', 'application/json');
break;
}

// Send the report as the response
res.send(report);
} catch (error) {
this.logger.error(`Failed to generate report: ${error.message}`, error.stack);
throw new HttpException('Failed to generate report', HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@Get('available')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Get list of available reports' })
@ApiResponse({ status: 200, description: 'Reports list retrieved successfully' })
async getAvailableReports(): Promise<{ reports: string[] }> {
try {
const reports = await this.reportingService.getAvailableReports();
return { reports };
} catch (error) {
this.logger.error(`Failed to get available reports: ${error.message}`, error.stack);
throw new HttpException('Failed to retrieve reports list', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// src/engagement-analytics/dto/create-engagement.dto.ts
import { IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import { EngagementType } from '../entities/engagement.entity';

export class CreateEngagementDto {
@IsNotEmpty()
@IsUUID()
userId: string;

@IsNotEmpty()
@IsUUID()
contentId: string;

@IsEnum(EngagementType)
type: EngagementType;

@IsOptional()
@IsString()
text?: string;

@IsOptional()
metadata?: Record<string, any>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// src/engagement-analytics/dto/engagement-query.dto.ts
import { IsDateString, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
import { Type } from 'class-transformer';

export enum TimeFrame {
HOUR = 'hour',
DAY = 'day',
WEEK = 'week',
MONTH = 'month',
CUSTOM = 'custom'
}

export class EngagementQueryDto {
@IsOptional()
@IsUUID()
contentId?: string;

@IsOptional()
@IsEnum(EngagementType, { each: true })
types?: EngagementType[];

@IsOptional()
@IsEnum(TimeFrame)
timeFrame?: TimeFrame = TimeFrame.DAY;

@IsOptional()
@IsDateString()
startDate?: string;

@IsOptional()
@IsDateString()
endDate?: string;

@IsOptional()
@Type(() => Number)
limit?: number = 100;

@IsOptional()
@Type(() => Number)
offset?: number = 0;

@IsOptional()
@IsString()
groupBy?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// src/engagement-analytics/dto/engagement-report.dto.ts
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';

export enum ReportFormat {
JSON = 'json',
CSV = 'csv',
PDF = 'pdf'
}

export enum ReportType {
DAILY = 'daily',
WEEKLY = 'weekly',
MONTHLY = 'monthly',
CUSTOM = 'custom'
}

export class EngagementReportDto {
@IsNotEmpty()
@IsEnum(ReportType)
type: ReportType;

@IsOptional()
@IsDateString()
startDate?: string;

@IsOptional()
@IsDateString()
endDate?: string;

@IsOptional()
@IsEnum(ReportFormat)
format?: ReportFormat = ReportFormat.JSON;

@IsOptional()
@IsString()
title?: string;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// src/engagement-analytics/entities/engagement.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, Index, ManyToOne } from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Content } from '../../content/entities/content.entity';

export enum EngagementType {
LIKE = 'like',
DISLIKE = 'dislike',
COMMENT = 'comment',
SHARE = 'share',
VIEW = 'view'
}

@Entity('engagements')
export class Engagement {
@PrimaryGeneratedColumn('uuid')
id: string;

@ManyToOne(() => User)
user: User;

@Column()
userId: string;

@ManyToOne(() => Content)
content: Content;

@Column()
contentId: string;

@Column({
type: 'enum',
enum: EngagementType,
})
type: EngagementType;

@Column({ nullable: true, type: 'text' })
text: string;

@Column({ default: 1 })
weight: number;

@CreateDateColumn()
@Index()
createdAt: Date;

@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@


// src/engagement-analytics/interfaces/engagement-analytics.interface.ts
import { EngagementType } from '../entities/engagement.entity';

export interface EngagementCount {
type: EngagementType;
count: number;
weightedScore: number;
}

export interface EngagementTimeSeries {
timestamp: Date;
engagements: EngagementCount[];
total: number;
weightedTotal: number;
}

export interface EngagementSummary {
contentId?: string;
period: {
start: Date;
end: Date;
};
totalEngagements: number;
engagementsByType: Record<EngagementType, number>;
weightedScores: Record<EngagementType, number>;
totalWeightedScore: number;
sentimentScore: number;
trendData: EngagementTimeSeries[];
anomalies: EngagementAnomaly[];
}

export interface EngagementAnomaly {
timestamp: Date;
type: EngagementType;
expected: number;
actual: number;
deviation: number;
significance: number;
}
Loading
Loading