@umituz/web-traffic 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +280 -0
- package/package.json +61 -0
- package/src/domains/affiliate/aggregates/affiliate.aggregate.ts +98 -0
- package/src/domains/affiliate/entities/affiliate-visit.entity.ts +52 -0
- package/src/domains/affiliate/index.ts +22 -0
- package/src/domains/affiliate/repositories/affiliate.repository.interface.ts +26 -0
- package/src/domains/affiliate/value-objects/affiliate-id.vo.ts +35 -0
- package/src/domains/affiliate/value-objects/site-id.vo.ts +33 -0
- package/src/domains/analytics/entities/analytics.entity.ts +33 -0
- package/src/domains/analytics/index.ts +18 -0
- package/src/domains/analytics/repositories/analytics.repository.interface.ts +19 -0
- package/src/domains/conversion/aggregates/order.aggregate.ts +85 -0
- package/src/domains/conversion/entities/order-item.entity.ts +27 -0
- package/src/domains/conversion/events/conversion-recorded.domain-event.ts +30 -0
- package/src/domains/conversion/index.ts +21 -0
- package/src/domains/conversion/repositories/conversion.repository.interface.ts +13 -0
- package/src/domains/conversion/value-objects/money.vo.ts +55 -0
- package/src/domains/tracking/aggregates/session.aggregate.ts +154 -0
- package/src/domains/tracking/application/tracking-command.service.ts +109 -0
- package/src/domains/tracking/entities/event.entity.ts +48 -0
- package/src/domains/tracking/entities/pageview.entity.ts +52 -0
- package/src/domains/tracking/events/event-tracked.domain-event.ts +28 -0
- package/src/domains/tracking/events/pageview-tracked.domain-event.ts +31 -0
- package/src/domains/tracking/index.ts +37 -0
- package/src/domains/tracking/repositories/event.repository.interface.ts +29 -0
- package/src/domains/tracking/value-objects/device-info.vo.ts +163 -0
- package/src/domains/tracking/value-objects/event-id.vo.ts +36 -0
- package/src/domains/tracking/value-objects/session-id.vo.ts +36 -0
- package/src/domains/tracking/value-objects/utm-parameters.vo.ts +75 -0
- package/src/index.ts +16 -0
- package/src/infrastructure/analytics/http-analytics.repository.impl.ts +60 -0
- package/src/infrastructure/index.ts +19 -0
- package/src/infrastructure/repositories/http-event.repository.impl.ts +160 -0
- package/src/infrastructure/tracking/web-traffic.service.ts +188 -0
- package/src/presentation/context.tsx +43 -0
- package/src/presentation/hooks.ts +78 -0
- package/src/presentation/index.ts +11 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Repository Interface
|
|
3
|
+
* @description Repository interface for Analytics queries (Domain Layer)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AnalyticsData } from '../entities/analytics.entity';
|
|
7
|
+
|
|
8
|
+
export interface AnalyticsQuery {
|
|
9
|
+
readonly startDate: Date;
|
|
10
|
+
readonly endDate: Date;
|
|
11
|
+
readonly path?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IAnalyticsRepository {
|
|
15
|
+
getAnalytics(query: AnalyticsQuery): Promise<AnalyticsData | null>;
|
|
16
|
+
getPageviews(query: AnalyticsQuery): Promise<number>;
|
|
17
|
+
getSessions(query: AnalyticsQuery): Promise<number>;
|
|
18
|
+
getVisitors(query: AnalyticsQuery): Promise<number>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Order Aggregate Root
|
|
3
|
+
* @description Manages conversion order and its items
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OrderItem } from '../entities/order-item.entity';
|
|
7
|
+
import type { EventId } from '../../tracking/value-objects/event-id.vo';
|
|
8
|
+
import { Money } from '../value-objects/money.vo';
|
|
9
|
+
|
|
10
|
+
export interface OrderCreateInput {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
orderId: string;
|
|
13
|
+
items: Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
price: number;
|
|
17
|
+
quantity: number;
|
|
18
|
+
}>;
|
|
19
|
+
currency?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class Order {
|
|
23
|
+
readonly id: EventId;
|
|
24
|
+
readonly sessionId: string;
|
|
25
|
+
readonly orderId: string;
|
|
26
|
+
private items: OrderItem[];
|
|
27
|
+
private total: Money;
|
|
28
|
+
readonly createdAt: number;
|
|
29
|
+
|
|
30
|
+
constructor(input: OrderCreateInput & { id: EventId }) {
|
|
31
|
+
this.id = input.id;
|
|
32
|
+
this.sessionId = input.sessionId;
|
|
33
|
+
this.orderId = input.orderId;
|
|
34
|
+
this.createdAt = Date.now();
|
|
35
|
+
this.items = [];
|
|
36
|
+
|
|
37
|
+
// Calculate total and create items
|
|
38
|
+
let totalAmount = 0;
|
|
39
|
+
for (const item of input.items) {
|
|
40
|
+
const orderItem: OrderItem = {
|
|
41
|
+
id: item.id,
|
|
42
|
+
name: item.name,
|
|
43
|
+
price: item.price,
|
|
44
|
+
quantity: item.quantity,
|
|
45
|
+
};
|
|
46
|
+
this.items.push(orderItem);
|
|
47
|
+
totalAmount += item.price * item.quantity;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.total = new Money(totalAmount, input.currency);
|
|
51
|
+
|
|
52
|
+
Object.freeze(this.items);
|
|
53
|
+
Object.freeze(this.id);
|
|
54
|
+
Object.freeze(this.sessionId);
|
|
55
|
+
Object.freeze(this.orderId);
|
|
56
|
+
Object.freeze(this.createdAt);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getTotal(): Money {
|
|
60
|
+
return this.total;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getItems(): OrderItem[] {
|
|
64
|
+
return [...this.items];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getItemCount(): number {
|
|
68
|
+
return this.items.reduce((sum, item) => sum + item.quantity, 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
isValid(): boolean {
|
|
72
|
+
return this.items.length > 0 && this.total.getAmount() > 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
toJSON() {
|
|
76
|
+
return {
|
|
77
|
+
id: this.id.toString(),
|
|
78
|
+
sessionId: this.sessionId,
|
|
79
|
+
orderId: this.orderId,
|
|
80
|
+
items: this.getItems(),
|
|
81
|
+
total: this.total.toJSON(),
|
|
82
|
+
createdAt: this.createdAt,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrderItem Entity
|
|
3
|
+
* @description Represents an item in a conversion order
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface OrderItem {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly price: number;
|
|
10
|
+
readonly quantity: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OrderItemCreateInput {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
price: number;
|
|
17
|
+
quantity: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createOrderItem(input: OrderItemCreateInput): OrderItem {
|
|
21
|
+
return {
|
|
22
|
+
id: input.id,
|
|
23
|
+
name: input.name,
|
|
24
|
+
price: input.price,
|
|
25
|
+
quantity: input.quantity,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversion Recorded Domain Event
|
|
3
|
+
* @description Published when a conversion is recorded
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Money } from '../value-objects/money.vo';
|
|
7
|
+
|
|
8
|
+
export class ConversionRecorded {
|
|
9
|
+
readonly eventType = 'ConversionRecorded';
|
|
10
|
+
readonly orderId: string;
|
|
11
|
+
readonly sessionId: string;
|
|
12
|
+
readonly revenue: Money;
|
|
13
|
+
readonly itemCount: number;
|
|
14
|
+
readonly occurredAt: number;
|
|
15
|
+
|
|
16
|
+
constructor(params: {
|
|
17
|
+
orderId: string;
|
|
18
|
+
sessionId: string;
|
|
19
|
+
revenue: Money;
|
|
20
|
+
itemCount: number;
|
|
21
|
+
occurredAt?: number;
|
|
22
|
+
}) {
|
|
23
|
+
this.orderId = params.orderId;
|
|
24
|
+
this.sessionId = params.sessionId;
|
|
25
|
+
this.revenue = params.revenue;
|
|
26
|
+
this.itemCount = params.itemCount;
|
|
27
|
+
this.occurredAt = params.occurredAt ?? Date.now();
|
|
28
|
+
Object.freeze(this);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversion Domain Export
|
|
3
|
+
* Subpath: @umituz/web-traffic/conversion
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Aggregates
|
|
7
|
+
export { Order } from './aggregates/order.aggregate';
|
|
8
|
+
export type { OrderCreateInput } from './aggregates/order.aggregate';
|
|
9
|
+
|
|
10
|
+
// Entities
|
|
11
|
+
export { createOrderItem } from './entities/order-item.entity';
|
|
12
|
+
export type { OrderItem, OrderItemCreateInput } from './entities/order-item.entity';
|
|
13
|
+
|
|
14
|
+
// Value Objects
|
|
15
|
+
export { Money } from './value-objects/money.vo';
|
|
16
|
+
|
|
17
|
+
// Repository Interfaces
|
|
18
|
+
export type { IConversionRepository } from './repositories/conversion.repository.interface';
|
|
19
|
+
|
|
20
|
+
// Domain Events
|
|
21
|
+
export { ConversionRecorded } from './events/conversion-recorded.domain-event';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversion Repository Interface
|
|
3
|
+
* @description Repository interface for Order persistence (Domain Layer)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Order } from '../aggregates/order.aggregate';
|
|
7
|
+
|
|
8
|
+
export interface IConversionRepository {
|
|
9
|
+
save(order: Order): Promise<void>;
|
|
10
|
+
findById(orderId: string): Promise<Order | null>;
|
|
11
|
+
findBySessionId(sessionId: string): Promise<Order[]>;
|
|
12
|
+
delete(orderId: string): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Money Value Object
|
|
3
|
+
* @description Immutable value object for monetary values
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class Money {
|
|
7
|
+
private readonly amount: number;
|
|
8
|
+
private readonly currency: string;
|
|
9
|
+
|
|
10
|
+
constructor(amount: number, currency: string = 'USD') {
|
|
11
|
+
if (amount < 0) {
|
|
12
|
+
throw new Error('Amount cannot be negative');
|
|
13
|
+
}
|
|
14
|
+
if (!currency || currency.length !== 3) {
|
|
15
|
+
throw new Error('Currency must be a valid ISO 4217 code');
|
|
16
|
+
}
|
|
17
|
+
this.amount = Math.round(amount * 100) / 100; // Round to 2 decimal places
|
|
18
|
+
this.currency = currency.toUpperCase();
|
|
19
|
+
Object.freeze(this);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getAmount(): number {
|
|
23
|
+
return this.amount;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getCurrency(): string {
|
|
27
|
+
return this.currency;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
add(other: Money): Money {
|
|
31
|
+
if (this.currency !== other.currency) {
|
|
32
|
+
throw new Error('Cannot add money with different currencies');
|
|
33
|
+
}
|
|
34
|
+
return new Money(this.amount + other.amount, this.currency);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
multiply(factor: number): Money {
|
|
38
|
+
return new Money(this.amount * factor, this.currency);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
equals(other: Money): boolean {
|
|
42
|
+
return this.amount === other.amount && this.currency === other.currency;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
toJSON() {
|
|
46
|
+
return {
|
|
47
|
+
amount: this.amount,
|
|
48
|
+
currency: this.currency,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static zero(currency: string = 'USD'): Money {
|
|
53
|
+
return new Money(0, currency);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Aggregate Root
|
|
3
|
+
* @description Manages user session and its events within consistency boundary
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Pageview } from '../entities/pageview.entity';
|
|
7
|
+
import type { Event } from '../entities/event.entity';
|
|
8
|
+
import { SessionId } from '../value-objects/session-id.vo';
|
|
9
|
+
import type { SiteId } from '../../affiliate/value-objects/site-id.vo';
|
|
10
|
+
import type { DeviceInfo } from '../value-objects/device-info.vo';
|
|
11
|
+
|
|
12
|
+
export interface SessionCreateInput {
|
|
13
|
+
deviceId: string;
|
|
14
|
+
siteId: SiteId;
|
|
15
|
+
deviceInfo: DeviceInfo;
|
|
16
|
+
startTime?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Session {
|
|
20
|
+
readonly id: SessionId;
|
|
21
|
+
readonly deviceId: string;
|
|
22
|
+
readonly siteId: SiteId;
|
|
23
|
+
readonly deviceInfo: DeviceInfo;
|
|
24
|
+
readonly startTime: number;
|
|
25
|
+
private endTime: number | null;
|
|
26
|
+
private events: Event[];
|
|
27
|
+
private pageviews: Pageview[];
|
|
28
|
+
private eventCount: number;
|
|
29
|
+
private pageviewCount: number;
|
|
30
|
+
private entryPage: string | null;
|
|
31
|
+
private exitPage: string | null;
|
|
32
|
+
|
|
33
|
+
constructor(input: SessionCreateInput & { id: SessionId }) {
|
|
34
|
+
this.id = input.id;
|
|
35
|
+
this.deviceId = input.deviceId;
|
|
36
|
+
this.siteId = input.siteId;
|
|
37
|
+
this.deviceInfo = input.deviceInfo;
|
|
38
|
+
this.startTime = input.startTime ?? Date.now();
|
|
39
|
+
this.endTime = null;
|
|
40
|
+
this.events = [];
|
|
41
|
+
this.pageviews = [];
|
|
42
|
+
this.eventCount = 0;
|
|
43
|
+
this.pageviewCount = 0;
|
|
44
|
+
this.entryPage = null;
|
|
45
|
+
this.exitPage = null;
|
|
46
|
+
Object.freeze(this.id);
|
|
47
|
+
Object.freeze(this.deviceId);
|
|
48
|
+
Object.freeze(this.siteId);
|
|
49
|
+
Object.freeze(this.deviceInfo);
|
|
50
|
+
Object.freeze(this.startTime);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Aggregate root methods - maintain consistency
|
|
54
|
+
addEvent(event: Event): void {
|
|
55
|
+
if (this.isExpired()) {
|
|
56
|
+
throw new Error('Cannot add event to expired session');
|
|
57
|
+
}
|
|
58
|
+
this.events.push(event);
|
|
59
|
+
this.eventCount++;
|
|
60
|
+
this.updateLastActivity();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
addPageview(pageview: Pageview): void {
|
|
64
|
+
if (this.isExpired()) {
|
|
65
|
+
throw new Error('Cannot add pageview to expired session');
|
|
66
|
+
}
|
|
67
|
+
this.pageviews.push(pageview);
|
|
68
|
+
this.pageviewCount++;
|
|
69
|
+
this.exitPage = pageview.path;
|
|
70
|
+
if (!this.entryPage) {
|
|
71
|
+
this.entryPage = pageview.path;
|
|
72
|
+
}
|
|
73
|
+
this.updateLastActivity();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getEntryPage(): string | null {
|
|
77
|
+
return this.entryPage;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getExitPage(): string | null {
|
|
81
|
+
return this.exitPage;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getSiteId(): SiteId {
|
|
85
|
+
return this.siteId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getVisitorId(): string {
|
|
89
|
+
return this.deviceId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getDeviceInfo(): DeviceInfo {
|
|
93
|
+
return this.deviceInfo;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
close(): void {
|
|
97
|
+
if (this.endTime) {
|
|
98
|
+
throw new Error('Session already closed');
|
|
99
|
+
}
|
|
100
|
+
this.endTime = Date.now();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
isExpired(timeoutMs: number = 30 * 60 * 1000): boolean {
|
|
104
|
+
if (this.endTime) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return Date.now() - this.startTime > timeoutMs;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
isActive(timeoutMs: number = 30 * 60 * 1000): boolean {
|
|
111
|
+
return !this.isExpired(timeoutMs);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getDuration(): number {
|
|
115
|
+
const end = this.endTime ?? Date.now();
|
|
116
|
+
return end - this.startTime;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getEventCount(): number {
|
|
120
|
+
return this.eventCount;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getPageviewCount(): number {
|
|
124
|
+
return this.pageviewCount;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getEvents(): Event[] {
|
|
128
|
+
return [...this.events];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getPageviews(): Pageview[] {
|
|
132
|
+
return [...this.pageviews];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private updateLastActivity(): void {
|
|
136
|
+
// Could emit domain event here
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
toJSON() {
|
|
140
|
+
return {
|
|
141
|
+
id: this.id.toString(),
|
|
142
|
+
deviceId: this.deviceId,
|
|
143
|
+
siteId: this.siteId.toString(),
|
|
144
|
+
deviceInfo: this.deviceInfo.toJSON(),
|
|
145
|
+
startTime: this.startTime,
|
|
146
|
+
endTime: this.endTime,
|
|
147
|
+
eventCount: this.eventCount,
|
|
148
|
+
pageviewCount: this.pageviewCount,
|
|
149
|
+
entryPage: this.entryPage,
|
|
150
|
+
exitPage: this.exitPage,
|
|
151
|
+
duration: this.getDuration(),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracking Command Service
|
|
3
|
+
* @description Application service for tracking commands (use-cases)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventId } from '../../tracking/value-objects/event-id.vo';
|
|
7
|
+
import { SessionId } from '../../tracking/value-objects/session-id.vo';
|
|
8
|
+
import { UTMParameters } from '../../tracking/value-objects/utm-parameters.vo';
|
|
9
|
+
import { Session } from '../../tracking/aggregates/session.aggregate';
|
|
10
|
+
import { Event } from '../../tracking/entities/event.entity';
|
|
11
|
+
import { Pageview } from '../../tracking/entities/pageview.entity';
|
|
12
|
+
import type { ISessionRepository, IEventRepository, IPageviewRepository } from '../../tracking/repositories/event.repository.interface';
|
|
13
|
+
import type { EventTracked } from '../events/event-tracked.domain-event';
|
|
14
|
+
import type { PageviewTracked } from '../events/pageview-tracked.domain-event';
|
|
15
|
+
|
|
16
|
+
export interface TrackingCommandResult {
|
|
17
|
+
success: boolean;
|
|
18
|
+
eventId?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class TrackingCommandService {
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly sessionRepo: ISessionRepository,
|
|
25
|
+
private readonly eventRepo: IEventRepository,
|
|
26
|
+
private readonly pageviewRepo: IPageviewRepository
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
async trackEvent(
|
|
30
|
+
sessionId: string,
|
|
31
|
+
eventName: string,
|
|
32
|
+
properties: Record<string, unknown> = {}
|
|
33
|
+
): Promise<TrackingCommandResult> {
|
|
34
|
+
try {
|
|
35
|
+
const sessionIdVo = new SessionId(sessionId);
|
|
36
|
+
const eventId = EventId.generate();
|
|
37
|
+
|
|
38
|
+
const event = new Event({
|
|
39
|
+
id: eventId,
|
|
40
|
+
sessionId: sessionIdVo,
|
|
41
|
+
name: eventName,
|
|
42
|
+
properties,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Get or create session
|
|
46
|
+
let session = await this.sessionRepo.findById(sessionIdVo);
|
|
47
|
+
if (!session) {
|
|
48
|
+
throw new Error('Session not found');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
session.addEvent(event);
|
|
52
|
+
await this.eventRepo.save(event);
|
|
53
|
+
await this.sessionRepo.save(session);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
eventId: eventId.toString(),
|
|
58
|
+
};
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async trackPageview(
|
|
68
|
+
sessionId: string,
|
|
69
|
+
path: string,
|
|
70
|
+
referrer: string | null = null,
|
|
71
|
+
utmParams?: { source?: string; medium?: string; campaign?: string; term?: string; content?: string }
|
|
72
|
+
): Promise<TrackingCommandResult> {
|
|
73
|
+
try {
|
|
74
|
+
const sessionIdVo = new SessionId(sessionId);
|
|
75
|
+
const pageviewId = EventId.generate();
|
|
76
|
+
|
|
77
|
+
const utmParameters = utmParams ? new UTMParameters(utmParams) : null;
|
|
78
|
+
|
|
79
|
+
const pageview = new Pageview({
|
|
80
|
+
id: pageviewId,
|
|
81
|
+
sessionId: sessionIdVo,
|
|
82
|
+
siteId: new SessionId(data.sessionId).getSiteId(),
|
|
83
|
+
path,
|
|
84
|
+
referrer,
|
|
85
|
+
utmParameters,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Get or create session
|
|
89
|
+
let session = await this.sessionRepo.findById(sessionIdVo);
|
|
90
|
+
if (!session) {
|
|
91
|
+
throw new Error('Session not found');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
session.addPageview(pageview);
|
|
95
|
+
await this.pageviewRepo.save(pageview);
|
|
96
|
+
await this.sessionRepo.save(session);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
eventId: pageviewId.toString(),
|
|
101
|
+
};
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Entity
|
|
3
|
+
* @description Represents a tracked analytics event using value objects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventId } from '../value-objects/event-id.vo';
|
|
7
|
+
import type { SessionId } from '../value-objects/session-id.vo';
|
|
8
|
+
|
|
9
|
+
export interface EventCreateInput {
|
|
10
|
+
sessionId: SessionId;
|
|
11
|
+
name: string;
|
|
12
|
+
properties: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Event {
|
|
16
|
+
readonly id: EventId;
|
|
17
|
+
readonly sessionId: SessionId;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly properties: Record<string, unknown>;
|
|
20
|
+
readonly timestamp: number;
|
|
21
|
+
|
|
22
|
+
constructor(input: EventCreateInput & { id: EventId; timestamp?: number }) {
|
|
23
|
+
this.id = input.id;
|
|
24
|
+
this.sessionId = input.sessionId;
|
|
25
|
+
this.name = input.name;
|
|
26
|
+
this.properties = { ...input.properties };
|
|
27
|
+
this.timestamp = input.timestamp ?? Date.now();
|
|
28
|
+
Object.freeze(this.properties);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
hasProperty(key: string): boolean {
|
|
32
|
+
return key in this.properties;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getProperty<T = unknown>(key: string): T | undefined {
|
|
36
|
+
return this.properties[key] as T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
toJSON() {
|
|
40
|
+
return {
|
|
41
|
+
id: this.id.toString(),
|
|
42
|
+
sessionId: this.sessionId.toString(),
|
|
43
|
+
name: this.name,
|
|
44
|
+
properties: this.properties,
|
|
45
|
+
timestamp: this.timestamp,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pageview Entity
|
|
3
|
+
* @description Represents a page view using value objects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventId } from '../value-objects/event-id.vo';
|
|
7
|
+
import type { SessionId } from '../value-objects/session-id.vo';
|
|
8
|
+
import type { SiteId } from '../../affiliate/value-objects/site-id.vo';
|
|
9
|
+
import { UTMParameters } from '../value-objects/utm-parameters.vo';
|
|
10
|
+
|
|
11
|
+
export interface PageviewCreateInput {
|
|
12
|
+
sessionId: SessionId;
|
|
13
|
+
siteId: SiteId;
|
|
14
|
+
path: string;
|
|
15
|
+
referrer: string | null;
|
|
16
|
+
utmParameters: UTMParameters | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Pageview {
|
|
20
|
+
readonly id: EventId;
|
|
21
|
+
readonly sessionId: SessionId;
|
|
22
|
+
readonly siteId: SiteId;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly referrer: string | null;
|
|
25
|
+
readonly utmParameters: UTMParameters | null;
|
|
26
|
+
readonly timestamp: number;
|
|
27
|
+
|
|
28
|
+
constructor(input: PageviewCreateInput & { id: EventId; timestamp?: number }) {
|
|
29
|
+
this.id = input.id;
|
|
30
|
+
this.sessionId = input.sessionId;
|
|
31
|
+
this.siteId = input.siteId;
|
|
32
|
+
this.path = input.path;
|
|
33
|
+
this.referrer = input.referrer;
|
|
34
|
+
this.utmParameters = input.utmParameters;
|
|
35
|
+
this.timestamp = input.timestamp ?? Date.now();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
hasUTMParameters(): boolean {
|
|
39
|
+
return this.utmParameters?.hasAnyUTM() ?? false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
toJSON() {
|
|
43
|
+
return {
|
|
44
|
+
id: this.id.toString(),
|
|
45
|
+
sessionId: this.sessionId.toString(),
|
|
46
|
+
path: this.path,
|
|
47
|
+
referrer: this.referrer,
|
|
48
|
+
utmParameters: this.utmParameters?.toJSON() ?? null,
|
|
49
|
+
timestamp: this.timestamp,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Tracked Domain Event
|
|
3
|
+
* @description Published when an analytics event is tracked
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { EventId } from '../../tracking/value-objects/event-id.vo';
|
|
7
|
+
import type { SessionId } from '../../tracking/value-objects/session-id.vo';
|
|
8
|
+
|
|
9
|
+
export class EventTracked {
|
|
10
|
+
readonly eventType = 'EventTracked';
|
|
11
|
+
readonly eventId: EventId;
|
|
12
|
+
readonly sessionId: SessionId;
|
|
13
|
+
readonly eventName: string;
|
|
14
|
+
readonly occurredAt: number;
|
|
15
|
+
|
|
16
|
+
constructor(params: {
|
|
17
|
+
eventId: EventId;
|
|
18
|
+
sessionId: SessionId;
|
|
19
|
+
eventName: string;
|
|
20
|
+
occurredAt?: number;
|
|
21
|
+
}) {
|
|
22
|
+
this.eventId = params.eventId;
|
|
23
|
+
this.sessionId = params.sessionId;
|
|
24
|
+
this.eventName = params.eventName;
|
|
25
|
+
this.occurredAt = params.occurredAt ?? Date.now();
|
|
26
|
+
Object.freeze(this);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pageview Tracked Domain Event
|
|
3
|
+
* @description Published when a pageview is tracked
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { EventId } from '../../tracking/value-objects/event-id.vo';
|
|
7
|
+
import type { SessionId } from '../../tracking/value-objects/session-id.vo';
|
|
8
|
+
|
|
9
|
+
export class PageviewTracked {
|
|
10
|
+
readonly eventType = 'PageviewTracked';
|
|
11
|
+
readonly pageviewId: EventId;
|
|
12
|
+
readonly sessionId: SessionId;
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly hasUTM: boolean;
|
|
15
|
+
readonly occurredAt: number;
|
|
16
|
+
|
|
17
|
+
constructor(params: {
|
|
18
|
+
pageviewId: EventId;
|
|
19
|
+
sessionId: SessionId;
|
|
20
|
+
path: string;
|
|
21
|
+
hasUTM: boolean;
|
|
22
|
+
occurredAt?: number;
|
|
23
|
+
}) {
|
|
24
|
+
this.pageviewId = params.pageviewId;
|
|
25
|
+
this.sessionId = params.sessionId;
|
|
26
|
+
this.path = params.path;
|
|
27
|
+
this.hasUTM = params.hasUTM;
|
|
28
|
+
this.occurredAt = params.occurredAt ?? Date.now();
|
|
29
|
+
Object.freeze(this);
|
|
30
|
+
}
|
|
31
|
+
}
|