wip
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '@gridpilot/shared/application';
|
||||
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
|
||||
import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository';
|
||||
import type { IAnalyticsSnapshotRepository } from '../../domain/repositories/IAnalyticsSnapshotRepository';
|
||||
@@ -47,56 +48,108 @@ export class GetEntityAnalyticsQuery
|
||||
constructor(
|
||||
private readonly pageViewRepository: IPageViewRepository,
|
||||
private readonly engagementRepository: IEngagementRepository,
|
||||
private readonly snapshotRepository: IAnalyticsSnapshotRepository
|
||||
private readonly snapshotRepository: IAnalyticsSnapshotRepository,
|
||||
private readonly logger: ILogger
|
||||
) {}
|
||||
|
||||
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
|
||||
const totalPageViews = await this.pageViewRepository.countByEntityId(
|
||||
input.entityType,
|
||||
input.entityId,
|
||||
since
|
||||
);
|
||||
let totalPageViews = 0;
|
||||
try {
|
||||
totalPageViews = await this.pageViewRepository.countByEntityId(
|
||||
input.entityType,
|
||||
input.entityId,
|
||||
since
|
||||
);
|
||||
this.logger.debug(`Total page views for entity ${input.entityId}: ${totalPageViews}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error counting total page views for entity ${input.entityId}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const uniqueVisitors = await this.pageViewRepository.countUniqueVisitors(
|
||||
input.entityType,
|
||||
input.entityId,
|
||||
since
|
||||
);
|
||||
let uniqueVisitors = 0;
|
||||
try {
|
||||
uniqueVisitors = await this.pageViewRepository.countUniqueVisitors(
|
||||
input.entityType,
|
||||
input.entityId,
|
||||
since
|
||||
);
|
||||
this.logger.debug(`Unique visitors for entity ${input.entityId}: ${uniqueVisitors}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error counting unique visitors for entity ${input.entityId}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const sponsorClicks = await this.engagementRepository.getSponsorClicksForEntity(
|
||||
input.entityId,
|
||||
since
|
||||
);
|
||||
let sponsorClicks = 0;
|
||||
try {
|
||||
sponsorClicks = await this.engagementRepository.getSponsorClicksForEntity(
|
||||
input.entityId,
|
||||
since
|
||||
);
|
||||
this.logger.debug(`Sponsor clicks for entity ${input.entityId}: ${sponsorClicks}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting sponsor clicks for entity ${input.entityId}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Calculate engagement score (weighted sum of actions)
|
||||
const engagementScore = await this.calculateEngagementScore(input.entityId, since);
|
||||
|
||||
let engagementScore = 0;
|
||||
try {
|
||||
engagementScore = await this.calculateEngagementScore(input.entityId, since);
|
||||
this.logger.debug(`Engagement score for entity ${input.entityId}: ${engagementScore}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error calculating engagement score for entity ${input.entityId}: ${error.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}`);
|
||||
|
||||
// Get previous period for trends
|
||||
const previousPeriodStart = this.getPreviousPeriodStart(since, period);
|
||||
const previousPageViews = await this.pageViewRepository.countByEntityId(
|
||||
input.entityType,
|
||||
input.entityId,
|
||||
previousPeriodStart
|
||||
) - totalPageViews;
|
||||
this.logger.debug(`Previous period start: ${previousPeriodStart.toISOString()}`);
|
||||
|
||||
const previousUniqueVisitors = await this.pageViewRepository.countUniqueVisitors(
|
||||
input.entityType,
|
||||
input.entityId,
|
||||
previousPeriodStart
|
||||
) - uniqueVisitors;
|
||||
let previousPageViews = 0;
|
||||
try {
|
||||
const fullPreviousPageViews = await this.pageViewRepository.countByEntityId(
|
||||
input.entityType,
|
||||
input.entityId,
|
||||
previousPeriodStart
|
||||
);
|
||||
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) {
|
||||
this.logger.error(`Error counting previous period page views for entity ${input.entityId}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
let previousUniqueVisitors = 0;
|
||||
try {
|
||||
const fullPreviousUniqueVisitors = await this.pageViewRepository.countUniqueVisitors(
|
||||
input.entityType,
|
||||
input.entityId,
|
||||
previousPeriodStart
|
||||
);
|
||||
previousUniqueVisitors = fullPreviousUniqueVisitors - uniqueVisitors; // This calculates change, not just previous period's total
|
||||
this.logger.debug(`Previous period full unique visitors: ${fullPreviousUniqueVisitors}, change: ${previousUniqueVisitors}`);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`Error counting previous period unique visitors for entity ${input.entityId}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const result: EntityAnalyticsOutput = {
|
||||
entityType: input.entityType,
|
||||
entityId: input.entityId,
|
||||
summary: {
|
||||
@@ -118,9 +171,12 @@ export class GetEntityAnalyticsQuery
|
||||
label: this.formatPeriodLabel(since, now),
|
||||
},
|
||||
};
|
||||
this.logger.info(`Successfully retrieved analytics for entity ${input.entityId}.`);
|
||||
return result;
|
||||
}
|
||||
|
||||
private getPeriodStartDate(now: Date, period: SnapshotPeriod): Date {
|
||||
this.logger.debug(`Calculating period start date for "${period}" from ${now.toISOString()}`);
|
||||
const start = new Date(now);
|
||||
switch (period) {
|
||||
case 'daily':
|
||||
@@ -133,10 +189,12 @@ export class GetEntityAnalyticsQuery
|
||||
start.setMonth(start.getMonth() - 1);
|
||||
break;
|
||||
}
|
||||
this.logger.debug(`Period start date calculated: ${start.toISOString()}`);
|
||||
return start;
|
||||
}
|
||||
|
||||
private getPreviousPeriodStart(currentStart: Date, period: SnapshotPeriod): Date {
|
||||
this.logger.debug(`Calculating previous period start date for "${period}" from ${currentStart.toISOString()}`);
|
||||
const start = new Date(currentStart);
|
||||
switch (period) {
|
||||
case 'daily':
|
||||
@@ -149,13 +207,23 @@ export class GetEntityAnalyticsQuery
|
||||
start.setMonth(start.getMonth() - 1);
|
||||
break;
|
||||
}
|
||||
this.logger.debug(`Previous period start date calculated: ${start.toISOString()}`);
|
||||
return start;
|
||||
}
|
||||
|
||||
private async calculateEngagementScore(entityId: string, since: Date): Promise<number> {
|
||||
// Base engagement from sponsor interactions
|
||||
const sponsorClicks = await this.engagementRepository.getSponsorClicksForEntity(entityId, since);
|
||||
return sponsorClicks * 10; // Weighted score
|
||||
this.logger.debug(`Calculating engagement score for entity ${entityId} since ${since.toISOString()}`);
|
||||
let sponsorClicks = 0;
|
||||
try {
|
||||
sponsorClicks = await this.engagementRepository.getSponsorClicksForEntity(entityId, since);
|
||||
this.logger.debug(`Sponsor clicks for engagement score for entity ${entityId}: ${sponsorClicks}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting sponsor clicks for engagement score for entity ${entityId}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
const score = sponsorClicks * 10; // Weighted score
|
||||
this.logger.debug(`Calculated engagement score for entity ${entityId}: ${score}`);
|
||||
return score;
|
||||
}
|
||||
|
||||
private determineTrustIndicator(
|
||||
@@ -163,8 +231,10 @@ export class GetEntityAnalyticsQuery
|
||||
uniqueVisitors: number,
|
||||
engagementScore: number
|
||||
): 'high' | 'medium' | 'low' {
|
||||
this.logger.debug(`Determining trust indicator with pageViews: ${pageViews}, uniqueVisitors: ${uniqueVisitors}, engagementScore: ${engagementScore}`);
|
||||
const engagementRate = pageViews > 0 ? engagementScore / pageViews : 0;
|
||||
const returningVisitorRate = pageViews > 0 ? (pageViews - uniqueVisitors) / pageViews : 0;
|
||||
this.logger.debug(`Engagement rate: ${engagementRate}, Returning visitor rate: ${returningVisitorRate}`);
|
||||
|
||||
if (engagementRate > 0.1 && returningVisitorRate > 0.3) {
|
||||
return 'high';
|
||||
@@ -180,20 +250,33 @@ export class GetEntityAnalyticsQuery
|
||||
uniqueVisitors: number,
|
||||
sponsorClicks: number
|
||||
): number {
|
||||
this.logger.debug(`Calculating exposure value with pageViews: ${pageViews}, uniqueVisitors: ${uniqueVisitors}, sponsorClicks: ${sponsorClicks}`);
|
||||
// Simple exposure value calculation (could be monetized)
|
||||
return (pageViews * 0.01) + (uniqueVisitors * 0.05) + (sponsorClicks * 0.50);
|
||||
const exposure = (pageViews * 0.01) + (uniqueVisitors * 0.05) + (sponsorClicks * 0.50);
|
||||
this.logger.debug(`Calculated exposure value: ${exposure}`);
|
||||
return exposure;
|
||||
}
|
||||
|
||||
private calculatePercentageChange(previous: number, current: number): number {
|
||||
if (previous === 0) return current > 0 ? 100 : 0;
|
||||
return Math.round(((current - previous) / previous) * 100);
|
||||
this.logger.debug(`Calculating percentage change from previous: ${previous} to current: ${current}`);
|
||||
if (previous === 0) {
|
||||
const change = current > 0 ? 100 : 0;
|
||||
this.logger.debug(`Percentage change (previous was 0): ${change}%`);
|
||||
return change;
|
||||
}
|
||||
const change = Math.round(((current - previous) / previous) * 100);
|
||||
this.logger.debug(`Percentage change: ${change}%`);
|
||||
return change;
|
||||
}
|
||||
|
||||
private formatPeriodLabel(start: Date, end: Date): string {
|
||||
this.logger.debug(`Formatting period label from ${start.toISOString()} to ${end.toISOString()}`);
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
return `${formatter.format(start)} - ${formatter.format(end)}`;
|
||||
const label = `${formatter.format(start)} - ${formatter.format(end)}`;
|
||||
this.logger.debug(`Formatted period label: "${label}"`);
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { EngagementEvent, type EngagementAction, type EngagementEntityType } from '../../domain/entities/EngagementEvent';
|
||||
import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository';
|
||||
|
||||
@@ -25,31 +26,41 @@ export interface RecordEngagementOutput {
|
||||
|
||||
export class RecordEngagementUseCase
|
||||
implements AsyncUseCase<RecordEngagementInput, RecordEngagementOutput> {
|
||||
constructor(private readonly engagementRepository: IEngagementRepository) {}
|
||||
constructor(
|
||||
private readonly engagementRepository: IEngagementRepository,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(input: RecordEngagementInput): Promise<RecordEngagementOutput> {
|
||||
const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.logger.debug('Executing RecordEngagementUseCase', { input });
|
||||
try {
|
||||
const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseProps: Omit<Parameters<typeof EngagementEvent.create>[0], 'timestamp'> = {
|
||||
id: eventId,
|
||||
action: input.action,
|
||||
entityType: input.entityType,
|
||||
entityId: input.entityId,
|
||||
actorType: input.actorType,
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
const baseProps: Omit<Parameters<typeof EngagementEvent.create>[0], 'timestamp'> = {
|
||||
id: eventId,
|
||||
action: input.action,
|
||||
entityType: input.entityType,
|
||||
entityId: input.entityId,
|
||||
actorType: input.actorType,
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
|
||||
const event = EngagementEvent.create({
|
||||
...baseProps,
|
||||
...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
|
||||
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
||||
});
|
||||
const event = EngagementEvent.create({
|
||||
...baseProps,
|
||||
...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
|
||||
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
||||
});
|
||||
|
||||
await this.engagementRepository.save(event);
|
||||
await this.engagementRepository.save(event);
|
||||
this.logger.info('Engagement recorded successfully', { eventId, input });
|
||||
|
||||
return {
|
||||
eventId,
|
||||
engagementWeight: event.getEngagementWeight(),
|
||||
};
|
||||
return {
|
||||
eventId,
|
||||
engagementWeight: event.getEngagementWeight(),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Error recording engagement', error, { input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { PageView } from '../../domain/entities/PageView';
|
||||
import type { EntityType, VisitorType } from '../../domain/types/PageView';
|
||||
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
|
||||
@@ -26,29 +27,38 @@ export interface RecordPageViewOutput {
|
||||
|
||||
export class RecordPageViewUseCase
|
||||
implements AsyncUseCase<RecordPageViewInput, RecordPageViewOutput> {
|
||||
constructor(private readonly pageViewRepository: IPageViewRepository) {}
|
||||
constructor(
|
||||
private readonly pageViewRepository: IPageViewRepository,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
|
||||
const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.logger.debug('Executing RecordPageViewUseCase', { input });
|
||||
try {
|
||||
const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseProps: Omit<Parameters<typeof PageView.create>[0], 'timestamp'> = {
|
||||
id: pageViewId,
|
||||
entityType: input.entityType,
|
||||
entityId: input.entityId,
|
||||
visitorType: input.visitorType,
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
const baseProps: Omit<Parameters<typeof PageView.create>[0], 'timestamp'> = {
|
||||
id: pageViewId,
|
||||
entityType: input.entityType,
|
||||
entityId: input.entityId,
|
||||
visitorType: input.visitorType,
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
|
||||
const pageView = PageView.create({
|
||||
...baseProps,
|
||||
...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}),
|
||||
...(input.referrer !== undefined ? { referrer: input.referrer } : {}),
|
||||
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
|
||||
...(input.country !== undefined ? { country: input.country } : {}),
|
||||
});
|
||||
const pageView = PageView.create({
|
||||
...baseProps,
|
||||
...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}),
|
||||
...(input.referrer !== undefined ? { referrer: input.referrer } : {}),
|
||||
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
|
||||
...(input.country !== undefined ? { country: input.country } : {}),
|
||||
});
|
||||
|
||||
await this.pageViewRepository.save(pageView);
|
||||
|
||||
return { pageViewId };
|
||||
await this.pageViewRepository.save(pageView);
|
||||
this.logger.info('Page view recorded successfully', { pageViewId, input });
|
||||
return { pageViewId };
|
||||
} catch (error) {
|
||||
this.logger.error('Error recording page view', error, { input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
packages/analytics/domain/ports/ILogger.ts
Normal file
6
packages/analytics/domain/ports/ILogger.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface ILogger {
|
||||
debug(message: string, ...args: any[]): void;
|
||||
info(message: string, ...args: any[]): void;
|
||||
warn(message: string, ...args: any[]): void;
|
||||
error(message: string, ...args: any[]): void;
|
||||
}
|
||||
@@ -6,22 +6,56 @@
|
||||
|
||||
import type { IAnalyticsSnapshotRepository } from '../../domain/repositories/IAnalyticsSnapshotRepository';
|
||||
import { AnalyticsSnapshot, type SnapshotPeriod, type SnapshotEntityType } from '../../domain/entities/AnalyticsSnapshot';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRepository {
|
||||
private snapshots: Map<string, AnalyticsSnapshot> = new Map();
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemoryAnalyticsSnapshotRepository initialized.');
|
||||
}
|
||||
|
||||
async save(snapshot: AnalyticsSnapshot): Promise<void> {
|
||||
this.snapshots.set(snapshot.id, snapshot);
|
||||
this.logger.debug(`Saving AnalyticsSnapshot: ${snapshot.id}`);
|
||||
try {
|
||||
this.snapshots.set(snapshot.id, snapshot);
|
||||
this.logger.info(`AnalyticsSnapshot ${snapshot.id} saved successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving AnalyticsSnapshot ${snapshot.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<AnalyticsSnapshot | null> {
|
||||
return this.snapshots.get(id) ?? null;
|
||||
this.logger.debug(`Finding AnalyticsSnapshot by ID: ${id}`);
|
||||
try {
|
||||
const snapshot = this.snapshots.get(id) ?? null;
|
||||
if (snapshot) {
|
||||
this.logger.info(`Found AnalyticsSnapshot with ID: ${id}`);
|
||||
} else {
|
||||
this.logger.warn(`AnalyticsSnapshot with ID ${id} not found.`);
|
||||
}
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding AnalyticsSnapshot by ID ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByEntity(entityType: SnapshotEntityType, entityId: string): Promise<AnalyticsSnapshot[]> {
|
||||
return Array.from(this.snapshots.values()).filter(
|
||||
s => s.entityType === entityType && s.entityId === entityId
|
||||
);
|
||||
this.logger.debug(`Finding AnalyticsSnapshots by Entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const snapshots = Array.from(this.snapshots.values()).filter(
|
||||
s => s.entityType === entityType && s.entityId === entityId
|
||||
);
|
||||
this.logger.info(`Found ${snapshots.length} AnalyticsSnapshots for entity ${entityId}.`);
|
||||
return snapshots;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding AnalyticsSnapshots for entity ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByPeriod(
|
||||
@@ -31,13 +65,25 @@ export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRe
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): Promise<AnalyticsSnapshot | null> {
|
||||
return Array.from(this.snapshots.values()).find(
|
||||
s => s.entityType === entityType &&
|
||||
s.entityId === entityId &&
|
||||
s.period === period &&
|
||||
s.startDate >= startDate &&
|
||||
s.endDate <= endDate
|
||||
) ?? null;
|
||||
this.logger.debug(`Finding AnalyticsSnapshot by Period for entity ${entityId}, period ${period}, from ${startDate.toISOString()} to ${endDate.toISOString()}`);
|
||||
try {
|
||||
const snapshot = Array.from(this.snapshots.values()).find(
|
||||
s => s.entityType === entityType &&
|
||||
s.entityId === entityId &&
|
||||
s.period === period &&
|
||||
s.startDate >= startDate &&
|
||||
s.endDate <= endDate
|
||||
) ?? null;
|
||||
if (snapshot) {
|
||||
this.logger.info(`Found AnalyticsSnapshot for entity ${entityId}, period ${period}.`);
|
||||
} else {
|
||||
this.logger.warn(`No AnalyticsSnapshot found for entity ${entityId}, period ${period}, from ${startDate.toISOString()} to ${endDate.toISOString()}.`);
|
||||
}
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding AnalyticsSnapshot by period for entity ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findLatest(
|
||||
@@ -45,11 +91,23 @@ export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRe
|
||||
entityId: string,
|
||||
period: SnapshotPeriod
|
||||
): Promise<AnalyticsSnapshot | null> {
|
||||
const matching = Array.from(this.snapshots.values())
|
||||
.filter(s => s.entityType === entityType && s.entityId === entityId && s.period === period)
|
||||
.sort((a, b) => b.endDate.getTime() - a.endDate.getTime());
|
||||
|
||||
return matching[0] ?? null;
|
||||
this.logger.debug(`Finding latest AnalyticsSnapshot for entity ${entityId}, period ${period}`);
|
||||
try {
|
||||
const matching = Array.from(this.snapshots.values())
|
||||
.filter(s => s.entityType === entityType && s.entityId === entityId && s.period === period)
|
||||
.sort((a, b) => b.endDate.getTime() - a.endDate.getTime());
|
||||
|
||||
const snapshot = matching[0] ?? null;
|
||||
if (snapshot) {
|
||||
this.logger.info(`Found latest AnalyticsSnapshot for entity ${entityId}, period ${period}.`);
|
||||
} else {
|
||||
this.logger.warn(`No latest AnalyticsSnapshot found for entity ${entityId}, period ${period}.`);
|
||||
}
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding latest AnalyticsSnapshot for entity ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getHistoricalSnapshots(
|
||||
@@ -58,10 +116,18 @@ export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRe
|
||||
period: SnapshotPeriod,
|
||||
limit: number
|
||||
): Promise<AnalyticsSnapshot[]> {
|
||||
return Array.from(this.snapshots.values())
|
||||
.filter(s => s.entityType === entityType && s.entityId === entityId && s.period === period)
|
||||
.sort((a, b) => b.endDate.getTime() - a.endDate.getTime())
|
||||
.slice(0, limit);
|
||||
this.logger.debug(`Getting historical AnalyticsSnapshots for entity ${entityId}, period ${period}, limit ${limit}`);
|
||||
try {
|
||||
const snapshots = Array.from(this.snapshots.values())
|
||||
.filter(s => s.entityType === entityType && s.entityId === entityId && s.period === period)
|
||||
.sort((a, b) => b.endDate.getTime() - a.endDate.getTime())
|
||||
.slice(0, limit);
|
||||
this.logger.info(`Found ${snapshots.length} historical AnalyticsSnapshots for entity ${entityId}, period ${period}.`);
|
||||
return snapshots;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting historical AnalyticsSnapshots for entity ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for testing
|
||||
|
||||
@@ -6,59 +6,135 @@
|
||||
|
||||
import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository';
|
||||
import { EngagementEvent, type EngagementAction, type EngagementEntityType } from '../../domain/entities/EngagementEvent';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryEngagementRepository implements IEngagementRepository {
|
||||
private events: Map<string, EngagementEvent> = new Map();
|
||||
private logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemoryEngagementRepository initialized.');
|
||||
}
|
||||
|
||||
async save(event: EngagementEvent): Promise<void> {
|
||||
this.events.set(event.id, event);
|
||||
this.logger.debug(`Attempting to save engagement event: ${event.id}`);
|
||||
try {
|
||||
this.events.set(event.id, event);
|
||||
this.logger.info(`Successfully saved engagement event: ${event.id}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving engagement event ${event.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<EngagementEvent | null> {
|
||||
return this.events.get(id) ?? null;
|
||||
this.logger.debug(`Attempting to find engagement event by ID: ${id}`);
|
||||
try {
|
||||
const event = this.events.get(id) ?? null;
|
||||
if (event) {
|
||||
this.logger.info(`Found engagement event by ID: ${id}`);
|
||||
} else {
|
||||
this.logger.warn(`Engagement event not found for ID: ${id}`);
|
||||
// The original was info, but if a requested ID is not found that's more of a warning than an info.
|
||||
}
|
||||
return event;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding engagement event by ID ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByEntityId(entityType: EngagementEntityType, entityId: string): Promise<EngagementEvent[]> {
|
||||
return Array.from(this.events.values()).filter(
|
||||
e => e.entityType === entityType && e.entityId === entityId
|
||||
);
|
||||
this.logger.debug(`Attempting to find engagement events for entityType: ${entityType}, entityId: ${entityId}`);
|
||||
try {
|
||||
const events = Array.from(this.events.values()).filter(
|
||||
e => e.entityType === entityType && e.entityId === entityId
|
||||
);
|
||||
this.logger.info(`Found ${events.length} engagement events for entityType: ${entityType}, entityId: ${entityId}`);
|
||||
return events;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding engagement events by entity ID ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByAction(action: EngagementAction): Promise<EngagementEvent[]> {
|
||||
return Array.from(this.events.values()).filter(
|
||||
e => e.action === action
|
||||
);
|
||||
this.logger.debug(`Attempting to find engagement events by action: ${action}`);
|
||||
try {
|
||||
const events = Array.from(this.events.values()).filter(
|
||||
e => e.action === action
|
||||
);
|
||||
this.logger.info(`Found ${events.length} engagement events for action: ${action}`);
|
||||
return events;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding engagement events by action ${action}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByDateRange(startDate: Date, endDate: Date): Promise<EngagementEvent[]> {
|
||||
return Array.from(this.events.values()).filter(
|
||||
e => e.timestamp >= startDate && e.timestamp <= endDate
|
||||
);
|
||||
this.logger.debug(`Attempting to find engagement events by date range: ${startDate.toISOString()} - ${endDate.toISOString()}`);
|
||||
try {
|
||||
const events = Array.from(this.events.values()).filter(
|
||||
e => e.timestamp >= startDate && e.timestamp <= endDate
|
||||
);
|
||||
this.logger.info(`Found ${events.length} engagement events for date range.`);
|
||||
return events;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding engagement events by date range:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async countByAction(action: EngagementAction, entityId?: string, since?: Date): Promise<number> {
|
||||
return Array.from(this.events.values()).filter(
|
||||
e => e.action === action &&
|
||||
(!entityId || e.entityId === entityId) &&
|
||||
(!since || e.timestamp >= since)
|
||||
).length;
|
||||
this.logger.debug(`Attempting to count engagement events for action: ${action}, entityId: ${entityId}, since: ${since?.toISOString()}`);
|
||||
try {
|
||||
const count = Array.from(this.events.values()).filter(
|
||||
e => e.action === action &&
|
||||
(!entityId || e.entityId === entityId) &&
|
||||
(!since || e.timestamp >= since)
|
||||
).length;
|
||||
this.logger.info(`Counted ${count} engagement events for action: ${action}, entityId: ${entityId}`);
|
||||
return count;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error counting engagement events by action ${action}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getSponsorClicksForEntity(entityId: string, since?: Date): Promise<number> {
|
||||
return Array.from(this.events.values()).filter(
|
||||
e => e.entityId === entityId &&
|
||||
(e.action === 'click_sponsor_logo' || e.action === 'click_sponsor_url') &&
|
||||
(!since || e.timestamp >= since)
|
||||
).length;
|
||||
this.logger.debug(`Attempting to get sponsor clicks for entity ID: ${entityId}, since: ${since?.toISOString()}`);
|
||||
try {
|
||||
const count = Array.from(this.events.values()).filter(
|
||||
e => e.entityId === entityId &&
|
||||
(e.action === 'click_sponsor_logo' || e.action === 'click_sponsor_url') &&
|
||||
(!since || e.timestamp >= since)
|
||||
).length;
|
||||
this.logger.info(`Counted ${count} sponsor clicks for entity ID: ${entityId}`);
|
||||
return count;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting sponsor clicks for entity ID ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for testing
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all engagement events.');
|
||||
this.events.clear();
|
||||
this.logger.info('All engagement events cleared.');
|
||||
}
|
||||
|
||||
// Helper for seeding demo data
|
||||
seed(events: EngagementEvent[]): void {
|
||||
events.forEach(e => this.events.set(e.id, e));
|
||||
this.logger.debug(`Seeding ${events.length} engagement events.`);
|
||||
try {
|
||||
events.forEach(e => this.events.set(e.id, e));
|
||||
this.logger.info(`Successfully seeded ${events.length} engagement events.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding engagement events:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,65 +6,139 @@
|
||||
|
||||
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
|
||||
import { PageView, type EntityType } from '../../domain/entities/PageView';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryPageViewRepository implements IPageViewRepository {
|
||||
private pageViews: Map<string, PageView> = new Map();
|
||||
private logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemoryPageViewRepository initialized.');
|
||||
}
|
||||
|
||||
async save(pageView: PageView): Promise<void> {
|
||||
this.pageViews.set(pageView.id, pageView);
|
||||
this.logger.debug(`Attempting to save page view: ${pageView.id}`);
|
||||
try {
|
||||
this.pageViews.set(pageView.id, pageView);
|
||||
this.logger.info(`Successfully saved page view: ${pageView.id}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving page view ${pageView.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<PageView | null> {
|
||||
return this.pageViews.get(id) ?? null;
|
||||
this.logger.debug(`Attempting to find page view by ID: ${id}`);
|
||||
try {
|
||||
const pageView = this.pageViews.get(id) ?? null;
|
||||
if (pageView) {
|
||||
this.logger.info(`Found page view by ID: ${id}`);
|
||||
} else {
|
||||
this.logger.warn(`Page view not found for ID: ${id}`);
|
||||
}
|
||||
return pageView;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding page view by ID ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByEntityId(entityType: EntityType, entityId: string): Promise<PageView[]> {
|
||||
return Array.from(this.pageViews.values()).filter(
|
||||
pv => pv.entityType === entityType && pv.entityId === entityId
|
||||
);
|
||||
this.logger.debug(`Attempting to find page views for entityType: ${entityType}, entityId: ${entityId}`);
|
||||
try {
|
||||
const pageViews = Array.from(this.pageViews.values()).filter(
|
||||
pv => pv.entityType === entityType && pv.entityId === entityId
|
||||
);
|
||||
this.logger.info(`Found ${pageViews.length} page views for entityType: ${entityType}, entityId: ${entityId}`);
|
||||
return pageViews;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding page views by entity ID ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByDateRange(startDate: Date, endDate: Date): Promise<PageView[]> {
|
||||
return Array.from(this.pageViews.values()).filter(
|
||||
pv => pv.timestamp >= startDate && pv.timestamp <= endDate
|
||||
);
|
||||
this.logger.debug(`Attempting to find page views by date range: ${startDate.toISOString()} - ${endDate.toISOString()}`);
|
||||
try {
|
||||
const pageViews = Array.from(this.pageViews.values()).filter(
|
||||
pv => pv.timestamp >= startDate && pv.timestamp <= endDate
|
||||
);
|
||||
this.logger.info(`Found ${pageViews.length} page views for date range.`);
|
||||
return pageViews;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding page views by date range:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findBySession(sessionId: string): Promise<PageView[]> {
|
||||
return Array.from(this.pageViews.values()).filter(
|
||||
pv => pv.sessionId === sessionId
|
||||
);
|
||||
this.logger.debug(`Attempting to find page views by session ID: ${sessionId}`);
|
||||
try {
|
||||
const pageViews = Array.from(this.pageViews.values()).filter(
|
||||
pv => pv.sessionId === sessionId
|
||||
);
|
||||
this.logger.info(`Found ${pageViews.length} page views for session ID: ${sessionId}`);
|
||||
return pageViews;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding page views by session ID ${sessionId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async countByEntityId(entityType: EntityType, entityId: string, since?: Date): Promise<number> {
|
||||
return Array.from(this.pageViews.values()).filter(
|
||||
pv => pv.entityType === entityType &&
|
||||
pv.entityId === entityId &&
|
||||
(!since || pv.timestamp >= since)
|
||||
).length;
|
||||
this.logger.debug(`Attempting to count page views for entityType: ${entityType}, entityId: ${entityId}, since: ${since?.toISOString()}`);
|
||||
try {
|
||||
const count = Array.from(this.pageViews.values()).filter(
|
||||
pv => pv.entityType === entityType &&
|
||||
pv.entityId === entityId &&
|
||||
(!since || pv.timestamp >= since)
|
||||
).length;
|
||||
this.logger.info(`Counted ${count} page views for entityType: ${entityType}, entityId: ${entityId}`);
|
||||
return count;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error counting page views by entity ID ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async countUniqueVisitors(entityType: EntityType, entityId: string, since?: Date): Promise<number> {
|
||||
const visitors = new Set<string>();
|
||||
Array.from(this.pageViews.values())
|
||||
.filter(
|
||||
pv => pv.entityType === entityType &&
|
||||
pv.entityId === entityId &&
|
||||
(!since || pv.timestamp >= since)
|
||||
)
|
||||
.forEach(pv => {
|
||||
visitors.add(pv.visitorId ?? pv.sessionId);
|
||||
});
|
||||
return visitors.size;
|
||||
this.logger.debug(`Attempting to count unique visitors for entityType: ${entityType}, entityId: ${entityId}, since: ${since?.toISOString()}`);
|
||||
try {
|
||||
const visitors = new Set<string>();
|
||||
Array.from(this.pageViews.values())
|
||||
.filter(
|
||||
pv => pv.entityType === entityType &&
|
||||
pv.entityId === entityId &&
|
||||
(!since || pv.timestamp >= since)
|
||||
)
|
||||
.forEach(pv => {
|
||||
visitors.add(pv.visitorId ?? pv.sessionId);
|
||||
});
|
||||
this.logger.info(`Counted ${visitors.size} unique visitors for entityType: ${entityType}, entityId: ${entityId}`);
|
||||
return visitors.size;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error counting unique visitors for entity ID ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for testing
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all page views.');
|
||||
this.pageViews.clear();
|
||||
this.logger.info('All page views cleared.');
|
||||
}
|
||||
|
||||
// Helper for seeding demo data
|
||||
seed(pageViews: PageView[]): void {
|
||||
pageViews.forEach(pv => this.pageViews.set(pv.id, pv));
|
||||
this.logger.debug(`Seeding ${pageViews.length} page views.`);
|
||||
try {
|
||||
pageViews.forEach(pv => this.pageViews.set(pv.id, pv));
|
||||
this.logger.info(`Successfully seeded ${pageViews.length} page views.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding page views:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user