Skip to content
Open
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
19 changes: 19 additions & 0 deletions backend/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { ReportQueryDto, ReportFormat } from './dto/report-query.dto';
import { ResolveMarketDto } from './dto/resolve-market.dto';
import { StatsResponseDto } from './dto/stats-response.dto';
import { UpdateUserRoleDto } from './dto/update-user-role.dto';
import { PlatformStatsDto } from './dto/platform-stats.dto';

type RequestUser = Request & { user: { id: string } };

Expand Down Expand Up @@ -67,6 +68,24 @@ export class AdminController {
return this.adminService.getFeeStats(query);
}

@Get('creator-events/stats')
@UseInterceptors(CacheInterceptor)
@CacheTTL(600) // 10 minutes
@ApiBearerAuth()
@ApiOperation({
summary: 'Get platform-wide statistics for creator events',
})
@ApiResponse({
status: 200,
description: 'Platform statistics',
type: PlatformStatsDto,
})
async getPlatformStats(
@Query() query: DateRangeQueryDto,
): Promise<PlatformStatsDto> {
return this.adminService.getPlatformStats(query);
}

@Delete('competitions/:id')
@ApiBearerAuth()
@ApiOperation({ summary: 'Cancel a competition' })
Expand Down
173 changes: 173 additions & 0 deletions backend/src/admin/admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import { SorobanService } from '../soroban/soroban.service';
import { User } from '../users/entities/user.entity';
import { CreatorEvent } from '../matches/entities/creator-event.entity';
import { Match } from '../matches/entities/match.entity';
import { MatchPrediction } from '../matches/entities/match-prediction.entity';
import { VerifiedAddress } from './entities/verified-address.entity';
import { FeeHistory } from '../indexer/entities/fee-history.entity';
import { ActivityLogQueryDto } from './dto/activity-log-query.dto';
Expand All @@ -39,6 +41,7 @@
import { ResolveMarketDto } from './dto/resolve-market.dto';
import { StatsResponseDto } from './dto/stats-response.dto';
import { UpdateUserRoleDto } from './dto/update-user-role.dto';
import { PlatformStatsDto } from './dto/platform-stats.dto';

@Injectable()
export class AdminService {
Expand All @@ -63,6 +66,10 @@
private readonly flagsRepository: Repository<Flag>,
@InjectRepository(CreatorEvent)
private readonly creatorEventRepository: Repository<CreatorEvent>,
@InjectRepository(Match)
private readonly matchRepository: Repository<Match>,
@InjectRepository(MatchPrediction)
private readonly matchPredictionRepository: Repository<MatchPrediction>,
@InjectRepository(VerifiedAddress)
private readonly verifiedAddressesRepository: Repository<VerifiedAddress>,
@InjectRepository(FeeHistory)
Expand Down Expand Up @@ -851,4 +858,170 @@
limit: take,
};
}

async getPlatformStats(range: DateRangeQueryDto): Promise<PlatformStatsDto> {
const startDate = range.start_date
? new Date(range.start_date)
: new Date(0);
const endDate = range.end_date ? new Date(range.end_date) : new Date();

const totalEventsCreated = range.start_date
? await this.creatorEventRepository.count({
where: { created_at: Between(startDate, endDate) },
})
: await this.creatorEventRepository.count();

const activeEventsCount = range.start_date
? await this.creatorEventRepository.count({
where: {
is_active: true,
is_cancelled: false,
created_at: Between(startDate, endDate),
},
})
: await this.creatorEventRepository.count({
where: { is_active: true, is_cancelled: false },
});

const completedEventsCount = await this.creatorEventRepository
.createQueryBuilder('event')
.innerJoinAndSelect(
'event.matches',
'match',
'match.result_submitted = true',
)
.distinct(true)
.getCount();

const cancelledEventsCount = range.start_date
? await this.creatorEventRepository.count({
where: {
is_cancelled: true,
created_at: Between(startDate, endDate),
},
})
: await this.creatorEventRepository.count({
where: { is_cancelled: true },
});

const uniqueParticipants = await this.matchPredictionRepository

Check warning on line 907 in backend/src/admin/admin.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
.createQueryBuilder('pred')
.select('COUNT(DISTINCT pred.user_id)', 'count')
.getRawOne();
const totalUniqueParticipants = parseInt(uniqueParticipants?.count || '0');

Check warning on line 911 in backend/src/admin/admin.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .count on an `any` value

Check warning on line 911 in backend/src/admin/admin.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `string`

const totalMatchesCreated = range.start_date
? await this.matchRepository.count({
where: { created_at: Between(startDate, endDate) },
})
: await this.matchRepository.count();

const totalPredictionsSubmitted = range.start_date
? await this.matchPredictionRepository.count({
where: { predicted_at: Between(startDate, endDate) },
})
: await this.matchPredictionRepository.count();

const totalFeesCollected = range.start_date

Check warning on line 925 in backend/src/admin/admin.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
? await this.creatorEventRepository
.createQueryBuilder('event')
.select('SUM(CAST(event.creation_fee_paid AS DECIMAL))', 'total')
.where('event.created_at >= :startDate', { startDate })
.andWhere('event.created_at <= :endDate', { endDate })
.getRawOne()
: await this.creatorEventRepository
.createQueryBuilder('event')
.select('SUM(CAST(event.creation_fee_paid AS DECIMAL))', 'total')
.getRawOne();
const totalFeesCollectedStroops = totalFeesCollected?.total

Check warning on line 936 in backend/src/admin/admin.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .total on an `any` value

Check warning on line 936 in backend/src/admin/admin.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
? totalFeesCollected.total.split('.')[0]

Check warning on line 937 in backend/src/admin/admin.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access [0] on an `any` value

Check warning on line 937 in backend/src/admin/admin.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .total on an `any` value

Check warning on line 937 in backend/src/admin/admin.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe call of an `any` typed value
: '0';

const avgParticipantsPerEvent =
totalEventsCreated > 0
? Math.round(totalUniqueParticipants / totalEventsCreated)
: 0;

const avgMatchesPerEvent =
totalEventsCreated > 0
? Math.round(totalMatchesCreated / totalEventsCreated)
: 0;

const avgPredictionsPerUser =
totalUniqueParticipants > 0
? Math.round(
(totalPredictionsSubmitted / totalUniqueParticipants) * 100,
) / 100
: 0;

const mostActiveCtor = await this.creatorEventRepository

Check warning on line 957 in backend/src/admin/admin.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
.createQueryBuilder('event')
.select('event.creator_address', 'creator_address')
.addSelect('COUNT(*)', 'event_count')
.where(
range.start_date
? 'event.created_at >= :startDate'
: '1=1',
range.start_date ? { startDate } : {},
)
.andWhere(
range.end_date
? 'event.created_at <= :endDate'
: '1=1',
range.end_date ? { endDate } : {},
)
.groupBy('event.creator_address')
.orderBy('event_count', 'DESC')
.limit(1)
.getRawOne();

const mostPopularEventData = await this.creatorEventRepository
.createQueryBuilder('event')
.select('event.title', 'title')
.addSelect('event.creator_address', 'creator_address')
.addSelect('event.participant_count', 'participant_count')
.where(
range.start_date
? 'event.created_at >= :startDate'
: '1=1',
range.start_date ? { startDate } : {},
)
.andWhere(
range.end_date
? 'event.created_at <= :endDate'
: '1=1',
range.end_date ? { endDate } : {},
)
.orderBy('event.participant_count', 'DESC')
.limit(1)
.getRawOne();

return {
total_events_created: totalEventsCreated,
active_events_count: activeEventsCount,
completed_events_count: completedEventsCount,
cancelled_events_count: cancelledEventsCount,
total_unique_participants: totalUniqueParticipants,
total_matches_created: totalMatchesCreated,
total_predictions_submitted: totalPredictionsSubmitted,
total_fees_collected_stroops: totalFeesCollectedStroops,
avg_participants_per_event: avgParticipantsPerEvent,
avg_matches_per_event: avgMatchesPerEvent,
avg_predictions_per_user: avgPredictionsPerUser,
most_active_creator: mostActiveCtor
? {
creator_address: mostActiveCtor.creator_address,
event_count: parseInt(mostActiveCtor.event_count),
}
: null,
most_popular_event: mostPopularEventData
? {
title: mostPopularEventData.title,
creator_address: mostPopularEventData.creator_address,
participant_count: mostPopularEventData.participant_count,
}
: null,
generated_at: new Date().toISOString(),
};
}
}
67 changes: 67 additions & 0 deletions backend/src/admin/dto/platform-stats.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ApiProperty } from '@nestjs/swagger';

export class CreatorStatsDto {
@ApiProperty({ example: 'creator_address_here' })
creator_address: string;

@ApiProperty({ example: 15 })
event_count: number;
}

export class EventStatsDto {
@ApiProperty({ example: 'Event Title' })
title: string;

@ApiProperty({ example: 150 })
participant_count: number;

@ApiProperty({ example: 'creator_address_here' })
creator_address: string;
}

export class PlatformStatsDto {
@ApiProperty({ example: 250 })
total_events_created: number;

@ApiProperty({ example: 45 })
active_events_count: number;

@ApiProperty({ example: 150 })
completed_events_count: number;

@ApiProperty({ example: 55 })
cancelled_events_count: number;

@ApiProperty({ example: 3500 })
total_unique_participants: number;

@ApiProperty({ example: 12000 })
total_matches_created: number;

@ApiProperty({ example: 95000 })
total_predictions_submitted: number;

@ApiProperty({
example: '125000000000',
description: 'Total fees collected in stroops (string bigint)',
})
total_fees_collected_stroops: string;

@ApiProperty({ example: 14 })
avg_participants_per_event: number;

@ApiProperty({ example: 48 })
avg_matches_per_event: number;

@ApiProperty({ example: 12.5 })
avg_predictions_per_user: number;

@ApiProperty({ type: CreatorStatsDto })
most_active_creator: CreatorStatsDto | null;

@ApiProperty({ type: EventStatsDto })
most_popular_event: EventStatsDto | null;

@ApiProperty({ example: '2024-06-02T10:00:00Z' })
generated_at: string;
}
32 changes: 32 additions & 0 deletions backend/src/analytics/analytics.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { MarketAnalyticsDto } from './dto/market-analytics.dto';
import { MarketHistoryResponseDto } from './dto/market-history.dto';
import { UserTrendsDto } from './dto/user-trends.dto';
import { CategoryAnalyticsResponseDto } from './dto/category-analytics.dto';
import { TrendingEventsResponseDto } from './dto/trending-events.dto';

@ApiTags('Analytics')
@Controller('analytics')
Expand Down Expand Up @@ -126,4 +127,35 @@ export class AnalyticsController {
async getCategoryAnalytics(): Promise<CategoryAnalyticsResponseDto> {
return this.analyticsService.getCategoryAnalytics();
}

@Get('trending')
@Public()
@UseInterceptors(CacheInterceptor)
@CacheTTL(600) // 10 minutes
@ApiOperation({ summary: 'Get trending events based on recent activity' })
@ApiQuery({
name: 'limit',
required: false,
type: Number,
description: 'Number of events to return (default: 10, max: 50)',
})
@ApiQuery({
name: 'timeWindow',
required: false,
type: String,
description: 'Time window for trending calculation (24h, 7d, 30d)',
enum: ['24h', '7d', '30d'],
})
@ApiResponse({
status: 200,
description:
'Trending events with score, activity count, and growth metrics',
type: TrendingEventsResponseDto,
})
async getTrendingEvents(
@Query('limit') limit?: number,
@Query('timeWindow') timeWindow?: '24h' | '7d' | '30d',
): Promise<TrendingEventsResponseDto> {
return this.analyticsService.getTrendingEvents(limit || 10, timeWindow || '24h');
}
}
Loading
Loading