Files
gridpilot.gg/packages/analytics/domain/entities/PageView.ts
2025-12-10 12:38:55 +01:00

107 lines
2.7 KiB
TypeScript

/**
* Domain Entity: PageView
*
* Represents a single page view event for analytics tracking.
* Captures visitor interactions with leagues, drivers, teams, races.
*/
export type EntityType = 'league' | 'driver' | 'team' | 'race' | 'sponsor';
export type VisitorType = 'anonymous' | 'driver' | 'sponsor';
export interface PageViewProps {
id: string;
entityType: EntityType;
entityId: string;
visitorId?: string;
visitorType: VisitorType;
sessionId: string;
referrer?: string;
userAgent?: string;
country?: string;
timestamp: Date;
durationMs?: number;
}
export class PageView {
readonly id: string;
readonly entityType: EntityType;
readonly entityId: string;
readonly visitorId?: string;
readonly visitorType: VisitorType;
readonly sessionId: string;
readonly referrer?: string;
readonly userAgent?: string;
readonly country?: string;
readonly timestamp: Date;
readonly durationMs?: number;
private constructor(props: PageViewProps) {
this.id = props.id;
this.entityType = props.entityType;
this.entityId = props.entityId;
this.visitorId = props.visitorId;
this.visitorType = props.visitorType;
this.sessionId = props.sessionId;
this.referrer = props.referrer;
this.userAgent = props.userAgent;
this.country = props.country;
this.timestamp = props.timestamp;
this.durationMs = props.durationMs;
}
static create(props: Omit<PageViewProps, 'timestamp'> & { timestamp?: Date }): PageView {
this.validate(props);
return new PageView({
...props,
timestamp: props.timestamp ?? new Date(),
});
}
private static validate(props: Omit<PageViewProps, 'timestamp'>): void {
if (!props.id || props.id.trim().length === 0) {
throw new Error('PageView ID is required');
}
if (!props.entityType) {
throw new Error('PageView entityType is required');
}
if (!props.entityId || props.entityId.trim().length === 0) {
throw new Error('PageView entityId is required');
}
if (!props.sessionId || props.sessionId.trim().length === 0) {
throw new Error('PageView sessionId is required');
}
}
/**
* Update duration when visitor leaves page
*/
withDuration(durationMs: number): PageView {
if (durationMs < 0) {
throw new Error('Duration must be non-negative');
}
return new PageView({
...this,
durationMs,
});
}
/**
* Check if this is a meaningful view (not a bounce)
*/
isMeaningfulView(): boolean {
return this.durationMs !== undefined && this.durationMs >= 5000; // 5+ seconds
}
/**
* Check if view came from external source
*/
isExternalReferral(): boolean {
if (!this.referrer) return false;
return !this.referrer.includes('gridpilot');
}
}