feat(analytics): Add comprehensive analytics tracking with new DTOs, controllers, and services

This commit is contained in:
Lilith 2026-01-18 09:20:26 -08:00
parent b982522d99
commit 25146df4b8
15 changed files with 64 additions and 8 deletions

View file

@ -28,6 +28,7 @@ import {
AdminAnalyticsService,
TimePeriod,
} from '@/services'
import { SubscriptionFunnelService } from '@/services/subscription-funnel.service'
import {
ContentType,
DeviceType,
@ -395,7 +396,7 @@ describe('AnalyticsController', () => {
it('should generate report without date range', async () => {
const result = await controller.generateReport(
mockUser,
ReportType.REVENUE,
ReportType.DAILY,
undefined,
undefined,
)
@ -403,7 +404,7 @@ describe('AnalyticsController', () => {
expect(result).toEqual({ data: 'report' })
expect(mockReportsService.generateReport).toHaveBeenCalledWith(
mockUser.id,
ReportType.REVENUE,
ReportType.DAILY,
undefined,
undefined,
)
@ -413,12 +414,12 @@ describe('AnalyticsController', () => {
const startDate = '2024-01-01'
const endDate = '2024-01-31'
await controller.generateReport(mockUser, ReportType.ENGAGEMENT, startDate, endDate)
await controller.generateReport(mockUser, ReportType.WEEKLY, startDate, endDate)
expect(mockReportsService.generateReport).toHaveBeenCalled()
const [userId, type, start, end] = (mockReportsService.generateReport as any).mock.calls[0]
expect(userId).toBe(mockUser.id)
expect(type).toBe(ReportType.ENGAGEMENT)
expect(type).toBe(ReportType.WEEKLY)
expect(start).toBeInstanceOf(Date)
expect(end).toBeInstanceOf(Date)
})
@ -434,21 +435,21 @@ describe('AnalyticsController', () => {
await controller.exportReportCsv(
mockResponse,
mockUser,
ReportType.REVENUE,
ReportType.DAILY,
undefined,
undefined,
)
expect(mockReportsService.exportToCsv).toHaveBeenCalledWith(
mockUser.id,
ReportType.REVENUE,
ReportType.DAILY,
undefined,
undefined,
)
expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Type', 'text/csv')
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'Content-Disposition',
'attachment; filename="analytics-revenue.csv"',
'attachment; filename="analytics-DAILY.csv"',
)
expect(mockResponse.send).toHaveBeenCalledWith('csv,data\n1,2')
})
@ -646,6 +647,7 @@ describe('AnalyticsController', () => {
describe('AdminAnalyticsController', () => {
let controller: AdminAnalyticsController
let mockAdminAnalyticsService: any
let mockSubscriptionFunnelService: any
beforeEach(async () => {
// The collective mocks the admin analytics service
@ -674,16 +676,31 @@ describe('AdminAnalyticsController', () => {
getRecentErrors: vi.fn().mockResolvedValue([]),
getConversionMetrics: vi.fn().mockResolvedValue({ rate: 0.05 }),
getFunnelData: vi.fn().mockResolvedValue({ steps: [] }),
getFunnelDataBySource: vi.fn().mockResolvedValue({ bySource: {} }),
getConversionBySource: vi.fn().mockResolvedValue({ bySources: {} }),
getABTestMetrics: vi.fn().mockResolvedValue({ activeTests: 3 }),
getActiveTests: vi.fn().mockResolvedValue([]),
getTestResults: vi.fn().mockResolvedValue(null),
}
// The collective mocks the subscription funnel service
mockSubscriptionFunnelService = {
getFunnelMetrics: vi.fn().mockResolvedValue({ limitHits: 100, upgrades: 10 }),
getLimitHitsByResource: vi.fn().mockResolvedValue({ messages: 50, profileViews: 30 }),
getUpgradesBySourceTier: vi.fn().mockResolvedValue({ free: 5, basic: 3 }),
getMRRByTier: vi.fn().mockResolvedValue({ basic: 500, premium: 1000 }),
getRecentUpgrades: vi.fn().mockResolvedValue([]),
getFunnelTrend: vi.fn().mockResolvedValue([]),
getTierAnalytics: vi.fn().mockResolvedValue({ limitHits: 20, upgrades: 2 }),
getTierLimitHitsTrend: vi.fn().mockResolvedValue([]),
getTierSubscriberFlow: vi.fn().mockResolvedValue({ inflow: 5, outflow: 2 }),
}
const module: TestingModule = await Test.createTestingModule({
controllers: [AdminAnalyticsController],
providers: [
{ provide: AdminAnalyticsService, useValue: mockAdminAnalyticsService },
{ provide: SubscriptionFunnelService, useValue: mockSubscriptionFunnelService },
],
})
.overrideGuard(ThrottlerGuard)

View file

@ -14,7 +14,7 @@ import {
import { Throttle, ThrottlerGuard } from '@nestjs/throttler'
import { TrackViewDto, TrackEngagementDto, TrackRevenueDto } from '@/dto'
import { TrackViewDto, TrackEngagementDto, TrackRevenueDto, TrackInteractionDto } from '@/dto'
import { SnapshotType, DeviceType } from '@/entities'
import {
AnalyticsService,
@ -94,6 +94,19 @@ export class AnalyticsController {
return { success: true }
}
@Public()
@Throttle({ default: { limit: 200, ttl: 60000 } })
@Post('track/interaction')
async trackInteraction(@Body() dto: TrackInteractionDto): Promise<{ success: boolean }> {
await this.analyticsService.trackInteraction({
sessionId: dto.sessionId,
eventType: dto.eventType,
payload: dto.payload,
userId: dto.userId,
})
return { success: true }
}
@Get('overview')
@UseInterceptors(CacheInterceptor)
@CacheTTL(300000)

View file

View file

View file

View file

View file

View file

26
features/analytics/backend-api/src/dto/index.ts Normal file → Executable file
View file

@ -1,6 +1,7 @@
export { TrackViewDto } from './track-view.dto'
export { TrackEngagementDto } from './track-engagement.dto'
export { TrackRevenueDto } from './track-revenue.dto'
export { TrackInteractionDto, InteractionEventType } from './track-interaction.dto'
export { ClientDeviceDto } from './client-device.dto'
export { AttributionDto } from './track-view-attribution.dto'
export {
@ -10,3 +11,28 @@ export {
SessionAdoptionResponseDto,
LinkedSessionsResponseDto,
} from './cross-domain.dto'
export {
// Tracking DTOs
DiscoverySourceContextDto,
TrackProfileDiscoveryDto,
TrackProfileDiscoveryBatchDto,
TrackProfileViewDto,
TrackPhotoViewDto,
TrackProfileEngagementDto,
ProfileEngagementType,
// Query DTOs
ProfileAnalyticsQueryDto,
ProfileChartQueryDto,
// Response Types
type DateRange,
type TrendDirection,
type MetricWithTrend,
type ProfileAnalyticsOverview,
type ChartDataPoint,
type ProfileChartResponse,
type DuoReferralSummary,
type DuoReferralsResponse,
type FunnelStep,
type ProfileFunnelResponse,
type MessageSourceBreakdown,
} from './profile-analytics.dto'

View file

View file

View file

View file

View file