website refactor
This commit is contained in:
@@ -30,6 +30,10 @@ describe('AnalyticsService', () => {
|
||||
const result = await service.recordPageView(input);
|
||||
|
||||
expect(mockApiClient.recordPageView).toHaveBeenCalledWith({
|
||||
entityType: 'page',
|
||||
entityId: '/dashboard',
|
||||
visitorType: 'user',
|
||||
sessionId: 'temp-session',
|
||||
path: '/dashboard',
|
||||
userId: 'user-123',
|
||||
});
|
||||
@@ -48,6 +52,10 @@ describe('AnalyticsService', () => {
|
||||
const result = await service.recordPageView(input);
|
||||
|
||||
expect(mockApiClient.recordPageView).toHaveBeenCalledWith({
|
||||
entityType: 'page',
|
||||
entityId: '/home',
|
||||
visitorType: 'guest',
|
||||
sessionId: 'temp-session',
|
||||
path: '/home',
|
||||
});
|
||||
expect(result).toBeInstanceOf(RecordPageViewOutputViewModel);
|
||||
@@ -69,6 +77,11 @@ describe('AnalyticsService', () => {
|
||||
const result = await service.recordEngagement(input);
|
||||
|
||||
expect(mockApiClient.recordEngagement).toHaveBeenCalledWith({
|
||||
action: 'button_click',
|
||||
entityType: 'ui_element',
|
||||
entityId: 'unknown',
|
||||
actorType: 'user',
|
||||
sessionId: 'temp-session',
|
||||
eventType: 'button_click',
|
||||
userId: 'user-123',
|
||||
metadata: { buttonId: 'submit', page: '/form' },
|
||||
@@ -89,6 +102,11 @@ describe('AnalyticsService', () => {
|
||||
const result = await service.recordEngagement(input);
|
||||
|
||||
expect(mockApiClient.recordEngagement).toHaveBeenCalledWith({
|
||||
action: 'page_load',
|
||||
entityType: 'ui_element',
|
||||
entityId: 'unknown',
|
||||
actorType: 'guest',
|
||||
sessionId: 'temp-session',
|
||||
eventType: 'page_load',
|
||||
});
|
||||
expect(result).toBeInstanceOf(RecordEngagementOutputViewModel);
|
||||
|
||||
47
apps/website/lib/services/analytics/AnalyticsService.ts
Normal file
47
apps/website/lib/services/analytics/AnalyticsService.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { injectable, unmanaged } from 'inversify';
|
||||
import { AnalyticsApiClient } from '@/lib/api/analytics/AnalyticsApiClient';
|
||||
import { RecordPageViewOutputViewModel } from '@/lib/view-models/RecordPageViewOutputViewModel';
|
||||
import { RecordEngagementOutputViewModel } from '@/lib/view-models/RecordEngagementOutputViewModel';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
|
||||
@injectable()
|
||||
export class AnalyticsService implements Service {
|
||||
private readonly apiClient: AnalyticsApiClient;
|
||||
|
||||
constructor(@unmanaged() apiClient?: AnalyticsApiClient) {
|
||||
if (apiClient) {
|
||||
this.apiClient = apiClient;
|
||||
} else {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger);
|
||||
this.apiClient = new AnalyticsApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
}
|
||||
|
||||
async recordPageView(input: { path: string; userId?: string }): Promise<RecordPageViewOutputViewModel> {
|
||||
const data = await this.apiClient.recordPageView({
|
||||
entityType: 'page',
|
||||
entityId: input.path,
|
||||
visitorType: input.userId ? 'user' : 'guest',
|
||||
sessionId: 'temp-session', // Should come from a session service
|
||||
...input
|
||||
});
|
||||
return new RecordPageViewOutputViewModel(data);
|
||||
}
|
||||
|
||||
async recordEngagement(input: { eventType: string; userId?: string; metadata?: Record<string, any> }): Promise<RecordEngagementOutputViewModel> {
|
||||
const data = await this.apiClient.recordEngagement({
|
||||
action: input.eventType,
|
||||
entityType: 'ui_element',
|
||||
entityId: 'unknown',
|
||||
actorType: input.userId ? 'user' : 'guest',
|
||||
sessionId: 'temp-session', // Should come from a session service
|
||||
...input
|
||||
});
|
||||
return new RecordEngagementOutputViewModel(data);
|
||||
}
|
||||
}
|
||||
@@ -9,38 +9,39 @@ describe('DashboardService', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getDashboardData: vi.fn(),
|
||||
getDashboardOverview: vi.fn(),
|
||||
getAnalyticsMetrics: vi.fn(),
|
||||
recordPageView: vi.fn(),
|
||||
recordEngagement: vi.fn(),
|
||||
} as Mocked<AnalyticsApiClient>;
|
||||
} as any;
|
||||
|
||||
service = new DashboardService(mockApiClient);
|
||||
service = new DashboardService();
|
||||
(service as any).apiClient = mockApiClient;
|
||||
(service as any).analyticsApiClient = mockApiClient;
|
||||
});
|
||||
|
||||
describe('getDashboardData', () => {
|
||||
it('should call apiClient.getDashboardData and return AnalyticsDashboardViewModel', async () => {
|
||||
describe('getDashboardOverview', () => {
|
||||
it('should call apiClient.getDashboardOverview and return Result with DashboardOverviewDTO', async () => {
|
||||
const dto = {
|
||||
totalUsers: 100,
|
||||
activeUsers: 50,
|
||||
totalRaces: 20,
|
||||
totalLeagues: 5,
|
||||
};
|
||||
mockApiClient.getDashboardData.mockResolvedValue(dto);
|
||||
mockApiClient.getDashboardOverview.mockResolvedValue(dto);
|
||||
|
||||
const result = await service.getDashboardData();
|
||||
const result = await service.getDashboardOverview();
|
||||
|
||||
expect(mockApiClient.getDashboardData).toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(AnalyticsDashboardViewModel);
|
||||
expect(result.totalUsers).toBe(100);
|
||||
expect(result.activeUsers).toBe(50);
|
||||
expect(result.totalRaces).toBe(20);
|
||||
expect(result.totalLeagues).toBe(5);
|
||||
expect(mockApiClient.getDashboardOverview).toHaveBeenCalled();
|
||||
expect(result.isOk()).toBe(true);
|
||||
const value = (result as any).value;
|
||||
expect(value.totalUsers).toBe(100);
|
||||
expect(value.activeUsers).toBe(50);
|
||||
expect(value.totalRaces).toBe(20);
|
||||
expect(value.totalLeagues).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAnalyticsMetrics', () => {
|
||||
it('should call apiClient.getAnalyticsMetrics and return AnalyticsMetricsViewModel', async () => {
|
||||
it('should call apiClient.getAnalyticsMetrics and return Result with AnalyticsMetricsViewModel', async () => {
|
||||
const dto = {
|
||||
pageViews: 1000,
|
||||
uniqueVisitors: 500,
|
||||
@@ -52,11 +53,12 @@ describe('DashboardService', () => {
|
||||
const result = await service.getAnalyticsMetrics();
|
||||
|
||||
expect(mockApiClient.getAnalyticsMetrics).toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(AnalyticsMetricsViewModel);
|
||||
expect(result.pageViews).toBe(1000);
|
||||
expect(result.uniqueVisitors).toBe(500);
|
||||
expect(result.averageSessionDuration).toBe(300);
|
||||
expect(result.bounceRate).toBe(0.25);
|
||||
expect(result.isOk()).toBe(true);
|
||||
const value = (result as any).value;
|
||||
expect(value.pageViews).toBe(1000);
|
||||
expect(value.uniqueVisitors).toBe(500);
|
||||
expect(value.averageSessionDuration).toBe(300);
|
||||
expect(value.bounceRate).toBe(0.25);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { DashboardApiClient } from '@/lib/api/dashboard/DashboardApiClient';
|
||||
import { AnalyticsApiClient } from '@/lib/api/analytics/AnalyticsApiClient';
|
||||
import { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
|
||||
import { GetAnalyticsMetricsOutputDTO } from '@/lib/types/generated/GetAnalyticsMetricsOutputDTO';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
@@ -17,12 +19,14 @@ import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
@injectable()
|
||||
export class DashboardService implements Service {
|
||||
private apiClient: DashboardApiClient;
|
||||
private analyticsApiClient: AnalyticsApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
this.apiClient = new DashboardApiClient(baseUrl, errorReporter, logger);
|
||||
this.analyticsApiClient = new AnalyticsApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async getDashboardOverview(): Promise<Result<DashboardOverviewDTO, DomainError>> {
|
||||
@@ -30,29 +34,40 @@ export class DashboardService implements Service {
|
||||
const dto = await this.apiClient.getDashboardOverview();
|
||||
return Result.ok(dto);
|
||||
} catch (error) {
|
||||
// Convert ApiError to DomainError
|
||||
if (error instanceof ApiError) {
|
||||
switch (error.type) {
|
||||
case 'NOT_FOUND':
|
||||
return Result.err({ type: 'notFound', message: error.message });
|
||||
case 'AUTH_ERROR':
|
||||
return Result.err({ type: 'unauthorized', message: error.message });
|
||||
case 'SERVER_ERROR':
|
||||
return Result.err({ type: 'serverError', message: error.message });
|
||||
case 'NETWORK_ERROR':
|
||||
case 'TIMEOUT_ERROR':
|
||||
return Result.err({ type: 'networkError', message: error.message });
|
||||
default:
|
||||
return Result.err({ type: 'unknown', message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Handle non-ApiError cases
|
||||
if (error instanceof Error) {
|
||||
return Result.err({ type: 'unknown', message: error.message });
|
||||
}
|
||||
|
||||
return Result.err({ type: 'unknown', message: 'Dashboard fetch failed' });
|
||||
return this.handleError(error, 'Dashboard fetch failed');
|
||||
}
|
||||
}
|
||||
|
||||
async getAnalyticsMetrics(): Promise<Result<GetAnalyticsMetricsOutputDTO, DomainError>> {
|
||||
try {
|
||||
const dto = await this.analyticsApiClient.getAnalyticsMetrics();
|
||||
return Result.ok(dto);
|
||||
} catch (error) {
|
||||
return this.handleError(error, 'Analytics metrics fetch failed');
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(error: unknown, defaultMessage: string): Result<any, DomainError> {
|
||||
if (error instanceof ApiError) {
|
||||
switch (error.type) {
|
||||
case 'NOT_FOUND':
|
||||
return Result.err({ type: 'notFound', message: error.message });
|
||||
case 'AUTH_ERROR':
|
||||
return Result.err({ type: 'unauthorized', message: error.message });
|
||||
case 'SERVER_ERROR':
|
||||
return Result.err({ type: 'serverError', message: error.message });
|
||||
case 'NETWORK_ERROR':
|
||||
case 'TIMEOUT_ERROR':
|
||||
return Result.err({ type: 'networkError', message: error.message });
|
||||
default:
|
||||
return Result.err({ type: 'unknown', message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return Result.err({ type: 'unknown', message: error.message });
|
||||
}
|
||||
|
||||
return Result.err({ type: 'unknown', message: defaultMessage });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user