107 lines
2.7 KiB
TypeScript
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');
|
|
}
|
|
} |