This commit is contained in:
2025-12-21 17:05:36 +01:00
parent 08b0d59e45
commit f2d8a23583
66 changed files with 1131 additions and 1342 deletions

View File

@@ -5,12 +5,14 @@
* Returns metrics formatted for display to sponsors and admins.
*/
import type { AsyncUseCase , Logger } from '@core/shared/application';
import type { AsyncUseCase , Logger, UseCaseOutputPort } from '@core/shared/application';
import type { IPageViewRepository } from '../repositories/IPageViewRepository';
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
import type { IAnalyticsSnapshotRepository } from '@core/analytics/domain/repositories/IAnalyticsSnapshotRepository';
import type { EntityType } from '../../domain/types/PageView';
import type { SnapshotPeriod } from '../../domain/types/AnalyticsSnapshot';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
export interface GetEntityAnalyticsInput {
entityType: EntityType;
@@ -42,89 +44,68 @@ export interface EntityAnalyticsOutput {
};
}
export type GetEntityAnalyticsErrorCode = 'REPOSITORY_ERROR';
export class GetEntityAnalyticsQuery
implements AsyncUseCase<GetEntityAnalyticsInput, EntityAnalyticsOutput> {
implements AsyncUseCase<GetEntityAnalyticsInput, EntityAnalyticsOutput, GetEntityAnalyticsErrorCode> {
constructor(
private readonly pageViewRepository: IPageViewRepository,
private readonly engagementRepository: IEngagementRepository,
private readonly snapshotRepository: IAnalyticsSnapshotRepository,
private readonly output: UseCaseOutputPort<Result<EntityAnalyticsOutput, ApplicationErrorCode<GetEntityAnalyticsErrorCode, { message: string }>>>,
private readonly logger: Logger
) {}
async execute(input: GetEntityAnalyticsInput): Promise<EntityAnalyticsOutput> {
this.logger.debug(`Executing GetEntityAnalyticsQuery with input: ${JSON.stringify(input)}`);
const period = input.period ?? 'weekly';
const now = new Date();
const since = input.since ?? this.getPeriodStartDate(now, period);
this.logger.debug(`Calculated period: ${period}, now: ${now.toISOString()}, since: ${since.toISOString()}`);
// Get current metrics
let totalPageViews = 0;
async execute(input: GetEntityAnalyticsInput): Promise<Result<EntityAnalyticsOutput, ApplicationErrorCode<GetEntityAnalyticsErrorCode, { message: string }>>> {
try {
this.logger.debug(`Executing GetEntityAnalyticsQuery with input: ${JSON.stringify(input)}`);
const period = input.period ?? 'weekly';
const now = new Date();
const since = input.since ?? this.getPeriodStartDate(now, period);
this.logger.debug(`Calculated period: ${period}, now: ${now.toISOString()}, since: ${since.toISOString()}`);
// Get current metrics
let totalPageViews = 0;
totalPageViews = await this.pageViewRepository.countByEntityId(
input.entityType,
input.entityId,
since
);
this.logger.debug(`Total page views for entity ${input.entityId}: ${totalPageViews}`);
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error(`Error counting total page views for entity ${input.entityId}: ${err.message}`);
throw error;
}
let uniqueVisitors = 0;
try {
let uniqueVisitors = 0;
uniqueVisitors = await this.pageViewRepository.countUniqueVisitors(
input.entityType,
input.entityId,
since
);
this.logger.debug(`Unique visitors for entity ${input.entityId}: ${uniqueVisitors}`);
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error(`Error counting unique visitors for entity ${input.entityId}: ${err.message}`);
throw error;
}
let sponsorClicks = 0;
try {
let sponsorClicks = 0;
sponsorClicks = await this.engagementRepository.getSponsorClicksForEntity(
input.entityId,
since
);
this.logger.debug(`Sponsor clicks for entity ${input.entityId}: ${sponsorClicks}`);
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error(`Error getting sponsor clicks for entity ${input.entityId}: ${err.message}`);
throw error;
}
// Calculate engagement score (weighted sum of actions)
let engagementScore = 0;
try {
// Calculate engagement score (weighted sum of actions)
let engagementScore = 0;
engagementScore = await this.calculateEngagementScore(input.entityId, since);
this.logger.debug(`Engagement score for entity ${input.entityId}: ${engagementScore}`);
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error(`Error calculating engagement score for entity ${input.entityId}: ${err.message}`);
throw error;
}
// Determine trust indicator
const trustIndicator = this.determineTrustIndicator(totalPageViews, uniqueVisitors, engagementScore);
this.logger.debug(`Trust indicator for entity ${input.entityId}: ${trustIndicator}`);
// Calculate exposure value (for sponsor ROI)
const exposureValue = this.calculateExposureValue(totalPageViews, uniqueVisitors, sponsorClicks);
this.logger.debug(`Exposure value for entity ${input.entityId}: ${exposureValue}`);
// Determine trust indicator
const trustIndicator = this.determineTrustIndicator(totalPageViews, uniqueVisitors, engagementScore);
this.logger.debug(`Trust indicator for entity ${input.entityId}: ${trustIndicator}`);
// Get previous period for trends
const previousPeriodStart = this.getPreviousPeriodStart(since, period);
this.logger.debug(`Previous period start: ${previousPeriodStart.toISOString()}`);
// Calculate exposure value (for sponsor ROI)
const exposureValue = this.calculateExposureValue(totalPageViews, uniqueVisitors, sponsorClicks);
this.logger.debug(`Exposure value for entity ${input.entityId}: ${exposureValue}`);
let previousPageViews = 0;
try {
// Get previous period for trends
const previousPeriodStart = this.getPreviousPeriodStart(since, period);
this.logger.debug(`Previous period start: ${previousPeriodStart.toISOString()}`);
let previousPageViews = 0;
const fullPreviousPageViews = await this.pageViewRepository.countByEntityId(
input.entityType,
input.entityId,
@@ -132,14 +113,8 @@ export class GetEntityAnalyticsQuery
);
previousPageViews = fullPreviousPageViews - totalPageViews; // This calculates change, not just previous period's total
this.logger.debug(`Previous period full page views: ${fullPreviousPageViews}, change: ${previousPageViews}`);
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error(`Error counting previous period page views for entity ${input.entityId}: ${err.message}`);
throw error;
}
let previousUniqueVisitors = 0;
try {
let previousUniqueVisitors = 0;
const fullPreviousUniqueVisitors = await this.pageViewRepository.countUniqueVisitors(
input.entityType,
input.entityId,
@@ -148,36 +123,42 @@ export class GetEntityAnalyticsQuery
previousUniqueVisitors = fullPreviousUniqueVisitors - uniqueVisitors; // This calculates change, not just previous period's total
this.logger.debug(`Previous period full unique visitors: ${fullPreviousUniqueVisitors}, change: ${previousUniqueVisitors}`);
const resultData: EntityAnalyticsOutput = {
entityType: input.entityType,
entityId: input.entityId,
summary: {
totalPageViews,
uniqueVisitors,
sponsorClicks,
engagementScore,
trustIndicator,
exposureValue,
},
trends: {
pageViewsChange: this.calculatePercentageChange(previousPageViews, totalPageViews),
uniqueVisitorsChange: this.calculatePercentageChange(previousUniqueVisitors, uniqueVisitors),
engagementChange: 0, // Would need historical engagement data
},
period: {
start: since,
end: now,
label: this.formatPeriodLabel(since, now),
},
};
const result = Result.ok<EntityAnalyticsOutput, ApplicationErrorCode<GetEntityAnalyticsErrorCode, { message: string }>>(resultData);
this.output.present(result);
this.logger.info(`Successfully retrieved analytics for entity ${input.entityId}.`);
return result;
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error(`Error counting previous period unique visitors for entity ${input.entityId}: ${err.message}`);
throw error;
const err = error as Error;
this.logger.error(`Failed to get entity analytics for ${input.entityId}: ${err.message}`, err);
const result = Result.err<EntityAnalyticsOutput, ApplicationErrorCode<GetEntityAnalyticsErrorCode, { message: string }>>({
code: 'REPOSITORY_ERROR',
details: { message: err.message ?? 'Failed to get entity analytics' },
});
this.output.present(result);
return result;
}
const result: EntityAnalyticsOutput = {
entityType: input.entityType,
entityId: input.entityId,
summary: {
totalPageViews,
uniqueVisitors,
sponsorClicks,
engagementScore,
trustIndicator,
exposureValue,
},
trends: {
pageViewsChange: this.calculatePercentageChange(previousPageViews, totalPageViews),
uniqueVisitorsChange: this.calculatePercentageChange(previousUniqueVisitors, uniqueVisitors),
engagementChange: 0, // Would need historical engagement data
},
period: {
start: since,
end: now,
label: this.formatPeriodLabel(since, now),
},
};
this.logger.info(`Successfully retrieved analytics for entity ${input.entityId}.`);
return result;
}
private getPeriodStartDate(now: Date, period: SnapshotPeriod): Date {