@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,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Traffic Service
|
|
3
|
+
* @description Main service for web-traffic (Facade pattern)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventId } from '../../domains/tracking/value-objects/event-id.vo';
|
|
7
|
+
import { SessionId } from '../../domains/tracking/value-objects/session-id.vo';
|
|
8
|
+
import { SiteId } from '../../domains/affiliate/value-objects/site-id.vo';
|
|
9
|
+
import { DeviceInfo } from '../../domains/tracking/value-objects/device-info.vo';
|
|
10
|
+
import { Session } from '../../domains/tracking/aggregates/session.aggregate';
|
|
11
|
+
import type { Event } from '../../domains/tracking/entities/event.entity';
|
|
12
|
+
import type { Pageview } from '../../domains/tracking/entities/pageview.entity';
|
|
13
|
+
import type { TrackingCommandResult } from '../../domains/tracking/application/tracking-command.service';
|
|
14
|
+
import { TrackingCommandService } from '../../domains/tracking/application/tracking-command.service';
|
|
15
|
+
import {
|
|
16
|
+
HTTPEventRepository,
|
|
17
|
+
HTTPPageviewRepository,
|
|
18
|
+
LocalSessionRepository,
|
|
19
|
+
} from '../repositories/http-event.repository.impl';
|
|
20
|
+
import { HTTPAnalyticsRepository } from '../analytics/http-analytics.repository.impl';
|
|
21
|
+
|
|
22
|
+
export interface WebTrafficConfig {
|
|
23
|
+
readonly apiKey: string;
|
|
24
|
+
readonly apiUrl?: string;
|
|
25
|
+
readonly autoTrack?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class WebTrafficService {
|
|
29
|
+
private initialized = false;
|
|
30
|
+
private commandService: TrackingCommandService | null = null;
|
|
31
|
+
private currentSession: Session | null = null;
|
|
32
|
+
private eventRepo: HTTPEventRepository | null = null;
|
|
33
|
+
private pageviewRepo: HTTPPageviewRepository | null = null;
|
|
34
|
+
private sessionRepo: LocalSessionRepository | null = null;
|
|
35
|
+
|
|
36
|
+
initialize(config: WebTrafficConfig): void {
|
|
37
|
+
if (this.initialized) {
|
|
38
|
+
console.warn('WebTrafficService already initialized');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const apiUrl = config.apiUrl ?? 'https://analytics.umituz.com';
|
|
43
|
+
|
|
44
|
+
// Initialize repositories
|
|
45
|
+
this.eventRepo = new HTTPEventRepository({ apiUrl, apiKey: config.apiKey });
|
|
46
|
+
this.pageviewRepo = new HTTPPageviewRepository({ apiUrl, apiKey: config.apiKey });
|
|
47
|
+
this.sessionRepo = new LocalSessionRepository();
|
|
48
|
+
|
|
49
|
+
// Initialize command service
|
|
50
|
+
this.commandService = new TrackingCommandService(
|
|
51
|
+
this.sessionRepo,
|
|
52
|
+
this.eventRepo,
|
|
53
|
+
this.pageviewRepo
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Get or create session
|
|
57
|
+
this.initializeSession();
|
|
58
|
+
|
|
59
|
+
// Setup auto-tracking if enabled
|
|
60
|
+
if (config.autoTrack && typeof window !== 'undefined') {
|
|
61
|
+
this.setupAutoTrack();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.initialized = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
isInitialized(): boolean {
|
|
68
|
+
return this.initialized;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async trackEvent(
|
|
72
|
+
name: string,
|
|
73
|
+
properties: Record<string, unknown> = {}
|
|
74
|
+
): Promise<TrackingCommandResult> {
|
|
75
|
+
if (!this.commandService || !this.currentSession) {
|
|
76
|
+
return { success: false, error: 'Service not initialized' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return this.commandService.trackEvent(
|
|
80
|
+
this.currentSession.id.toString(),
|
|
81
|
+
name,
|
|
82
|
+
properties
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async trackPageView(path?: string): Promise<TrackingCommandResult> {
|
|
87
|
+
if (!this.commandService || !this.currentSession) {
|
|
88
|
+
return { success: false, error: 'Service not initialized' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const currentPath = path ?? (typeof window !== 'undefined' ? window.location.pathname : '/');
|
|
92
|
+
const referrer = typeof window !== 'undefined' ? document.referrer : null;
|
|
93
|
+
|
|
94
|
+
return this.commandService.trackPageview(
|
|
95
|
+
this.currentSession.id.toString(),
|
|
96
|
+
currentPath,
|
|
97
|
+
referrer,
|
|
98
|
+
this.getUTMFromURL()
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async initializeSession(): Promise<void> {
|
|
103
|
+
if (!this.sessionRepo) return;
|
|
104
|
+
|
|
105
|
+
const deviceId = this.getOrCreateDeviceId();
|
|
106
|
+
const existingSession = await this.sessionRepo.findActive(deviceId);
|
|
107
|
+
|
|
108
|
+
if (existingSession) {
|
|
109
|
+
this.currentSession = existingSession;
|
|
110
|
+
} else {
|
|
111
|
+
const sessionId = SessionId.generate();
|
|
112
|
+
const siteId = SiteId.generate();
|
|
113
|
+
const deviceInfo = DeviceInfo.fromUserAgent(
|
|
114
|
+
typeof window !== 'undefined' ? navigator.userAgent : '',
|
|
115
|
+
typeof window !== 'undefined' ? window.screen.width : undefined
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
this.currentSession = new Session({
|
|
119
|
+
id: sessionId,
|
|
120
|
+
deviceId,
|
|
121
|
+
siteId,
|
|
122
|
+
deviceInfo,
|
|
123
|
+
});
|
|
124
|
+
await this.sessionRepo.save(this.currentSession);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private getOrCreateDeviceId(): string {
|
|
129
|
+
if (typeof window === 'undefined') return '';
|
|
130
|
+
|
|
131
|
+
let deviceId = localStorage.getItem('wt_device_id');
|
|
132
|
+
if (!deviceId) {
|
|
133
|
+
deviceId = `device-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
134
|
+
localStorage.setItem('wt_device_id', deviceId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return deviceId;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private getUTMFromURL() {
|
|
141
|
+
if (typeof window === 'undefined') return undefined;
|
|
142
|
+
|
|
143
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
144
|
+
const source = searchParams.get('utm_source') || undefined;
|
|
145
|
+
const medium = searchParams.get('utm_medium') || undefined;
|
|
146
|
+
const campaign = searchParams.get('utm_campaign') || undefined;
|
|
147
|
+
const term = searchParams.get('utm_term') || undefined;
|
|
148
|
+
const content = searchParams.get('utm_content') || undefined;
|
|
149
|
+
|
|
150
|
+
if (!source && !medium && !campaign && !term && !content) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { source, medium, campaign, term, content };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private setupAutoTrack(): void {
|
|
158
|
+
if (typeof window === 'undefined') return;
|
|
159
|
+
|
|
160
|
+
// Track initial pageview
|
|
161
|
+
this.trackPageView();
|
|
162
|
+
|
|
163
|
+
// Track SPA navigation
|
|
164
|
+
const originalPushState = history.pushState;
|
|
165
|
+
const originalReplaceState = history.replaceState;
|
|
166
|
+
|
|
167
|
+
history.pushState = (...args) => {
|
|
168
|
+
originalPushState.apply(history, args);
|
|
169
|
+
this.trackPageView();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
history.replaceState = (...args) => {
|
|
173
|
+
originalReplaceState.apply(history, args);
|
|
174
|
+
this.trackPageView();
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
window.addEventListener('popstate', () => {
|
|
178
|
+
this.trackPageView();
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
destroy(): void {
|
|
183
|
+
this.eventRepo?.destroy();
|
|
184
|
+
this.initialized = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const webTrafficService = new WebTrafficService();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Traffic React Context
|
|
3
|
+
* @description React context provider for web-traffic tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { createContext } from 'react';
|
|
7
|
+
import { createContext, useContext, useEffect, useMemo, type ReactNode } from 'react';
|
|
8
|
+
import { webTrafficService, type WebTrafficConfig } from '../infrastructure/tracking/web-traffic.service';
|
|
9
|
+
import type { WebTrafficContextValue, TrackingCommandResult } from './hooks';
|
|
10
|
+
|
|
11
|
+
const TrackingContext = createContext<WebTrafficContextValue | null>(null);
|
|
12
|
+
|
|
13
|
+
export interface WebTrafficProviderProps {
|
|
14
|
+
readonly children: ReactNode;
|
|
15
|
+
readonly config: WebTrafficConfig;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function WebTrafficProvider({ children, config }: WebTrafficProviderProps): React.ReactElement {
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
webTrafficService.initialize(config);
|
|
21
|
+
|
|
22
|
+
return () => {
|
|
23
|
+
webTrafficService.destroy();
|
|
24
|
+
};
|
|
25
|
+
}, [config]);
|
|
26
|
+
|
|
27
|
+
const value = useMemo<WebTrafficContextValue>(
|
|
28
|
+
() => ({
|
|
29
|
+
trackEvent: async (name: string, properties?: Record<string, unknown>) => {
|
|
30
|
+
return webTrafficService.trackEvent(name, properties);
|
|
31
|
+
},
|
|
32
|
+
trackPageView: async (path?: string) => {
|
|
33
|
+
return webTrafficService.trackPageView(path);
|
|
34
|
+
},
|
|
35
|
+
isInitialized: webTrafficService.isInitialized(),
|
|
36
|
+
}),
|
|
37
|
+
[]
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return <TrackingContext.Provider value={value}>{children}</TrackingContext.Provider>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { TrackingContext as WebTrafficContext };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Traffic React Hooks
|
|
3
|
+
* @description React hooks for web-traffic tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useContext, useCallback, useEffect, useState } from 'react';
|
|
7
|
+
import { WebTrafficContext } from './context';
|
|
8
|
+
import { webTrafficService } from '../infrastructure/tracking/web-traffic.service';
|
|
9
|
+
import { HTTPAnalyticsRepository } from '../infrastructure/analytics/http-analytics.repository.impl';
|
|
10
|
+
import type { AnalyticsQuery } from '../domains/analytics/repositories/analytics.repository.interface';
|
|
11
|
+
import type { AnalyticsData } from '../domains/analytics/entities/analytics.entity';
|
|
12
|
+
|
|
13
|
+
export interface TrackingCommandResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
eventId?: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WebTrafficContextValue {
|
|
20
|
+
readonly trackEvent: (name: string, properties?: Record<string, unknown>) => Promise<TrackingCommandResult>;
|
|
21
|
+
readonly trackPageView: (path?: string) => Promise<TrackingCommandResult>;
|
|
22
|
+
readonly isInitialized: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useWebTraffic(): WebTrafficContextValue {
|
|
26
|
+
const context = useContext(WebTrafficContext);
|
|
27
|
+
|
|
28
|
+
if (!context) {
|
|
29
|
+
// If no context, use service directly
|
|
30
|
+
return {
|
|
31
|
+
trackEvent: useCallback(async (name: string, properties?: Record<string, unknown>) => {
|
|
32
|
+
return webTrafficService.trackEvent(name, properties);
|
|
33
|
+
}, []),
|
|
34
|
+
trackPageView: useCallback(async (path?: string) => {
|
|
35
|
+
return webTrafficService.trackPageView(path);
|
|
36
|
+
}, []),
|
|
37
|
+
isInitialized: webTrafficService.isInitialized(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return context;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useAnalytics(query: AnalyticsQuery) {
|
|
45
|
+
const [data, setData] = useState<AnalyticsData | null>(null);
|
|
46
|
+
const [loading, setLoading] = useState(true);
|
|
47
|
+
const [error, setError] = useState<Error | null>(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
let cancelled = false;
|
|
51
|
+
|
|
52
|
+
async function fetchAnalytics() {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
setError(null);
|
|
55
|
+
|
|
56
|
+
// Note: You'll need to initialize this with proper config
|
|
57
|
+
const repo = new HTTPAnalyticsRepository({
|
|
58
|
+
apiUrl: 'https://analytics.umituz.com',
|
|
59
|
+
apiKey: 'your-api-key', // This should come from config
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = await repo.getAnalytics(query);
|
|
63
|
+
|
|
64
|
+
if (!cancelled) {
|
|
65
|
+
setData(result);
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fetchAnalytics();
|
|
71
|
+
|
|
72
|
+
return () => {
|
|
73
|
+
cancelled = true;
|
|
74
|
+
};
|
|
75
|
+
}, [query]);
|
|
76
|
+
|
|
77
|
+
return { data, loading, error };
|
|
78
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation Layer Export
|
|
3
|
+
* Subpath: @umituz/web-traffic/presentation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { useWebTraffic, useAnalytics } from './hooks';
|
|
7
|
+
export type { WebTrafficContextValue } from './hooks';
|
|
8
|
+
|
|
9
|
+
export { WebTrafficProvider } from './context';
|
|
10
|
+
export { WebTrafficContext } from './context';
|
|
11
|
+
export type { WebTrafficProviderProps } from './context';
|