/** * Domain Entity: PageView * * Represents a single page view event for analytics tracking. * Captures visitor interactions with leagues, drivers, teams, races. */ import type { IEntity } from '@gridpilot/shared/domain'; import type { EntityType, VisitorType, PageViewProps } from '../types/PageView'; export type { EntityType, VisitorType } from '../types/PageView'; import { AnalyticsEntityId } from '../value-objects/AnalyticsEntityId'; import { AnalyticsSessionId } from '../value-objects/AnalyticsSessionId'; import { PageViewId } from '../value-objects/PageViewId'; export class PageView implements IEntity { readonly entityType: EntityType; readonly visitorId: string | undefined; readonly visitorType: VisitorType; readonly referrer: string | undefined; readonly userAgent: string | undefined; readonly country: string | undefined; readonly timestamp: Date; readonly durationMs: number | undefined; private readonly idVo: PageViewId; private readonly entityIdVo: AnalyticsEntityId; private readonly sessionIdVo: AnalyticsSessionId; private constructor(props: PageViewProps) { this.idVo = PageViewId.create(props.id); this.entityType = props.entityType; this.entityIdVo = AnalyticsEntityId.create(props.entityId); this.visitorId = props.visitorId; this.visitorType = props.visitorType; this.sessionIdVo = AnalyticsSessionId.create(props.sessionId); this.referrer = props.referrer; this.userAgent = props.userAgent; this.country = props.country; this.timestamp = props.timestamp; this.durationMs = props.durationMs; } get id(): string { return this.idVo.value; } get entityId(): string { return this.entityIdVo.value; } get sessionId(): string { return this.sessionIdVo.value; } static create(props: Omit & { timestamp?: Date }): PageView { this.validate(props); const baseProps: PageViewProps = { id: props.id, entityType: props.entityType, entityId: props.entityId, visitorType: props.visitorType, sessionId: props.sessionId, timestamp: props.timestamp ?? new Date(), ...(props.visitorId !== undefined ? { visitorId: props.visitorId } : {}), ...(props.referrer !== undefined ? { referrer: props.referrer } : {}), ...(props.userAgent !== undefined ? { userAgent: props.userAgent } : {}), ...(props.country !== undefined ? { country: props.country } : {}), ...(props.durationMs !== undefined ? { durationMs: props.durationMs } : {}), }; return new PageView(baseProps); } private static validate(props: Omit): 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 PageView.create({ id: this.id, entityType: this.entityType, entityId: this.entityId, visitorType: this.visitorType, sessionId: this.sessionId, timestamp: this.timestamp, ...(this.visitorId !== undefined ? { visitorId: this.visitorId } : {}), ...(this.referrer !== undefined ? { referrer: this.referrer } : {}), ...(this.userAgent !== undefined ? { userAgent: this.userAgent } : {}), ...(this.country !== undefined ? { country: this.country } : {}), 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'); } }