@umituz/web-traffic 1.0.9 → 1.0.11
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/package.json +1 -1
- package/src/domains/affiliate/value-objects/site-id.vo.ts +4 -1
- package/src/domains/tracking/application/tracking-command.service.ts +0 -2
- package/src/domains/tracking/repositories/event.repository.interface.ts +2 -1
- package/src/domains/tracking/value-objects/device-info.vo.ts +5 -2
- package/src/domains/tracking/value-objects/event-id.vo.ts +4 -1
- package/src/domains/tracking/value-objects/session-id.vo.ts +4 -1
- package/src/infrastructure/analytics/http-analytics.repository.impl.ts +1 -3
- package/src/infrastructure/repositories/http-event.repository.impl.ts +43 -17
- package/src/infrastructure/tracking/web-traffic.service.ts +54 -19
- package/src/presentation/context.tsx +2 -1
- package/src/presentation/hooks.ts +2 -18
package/package.json
CHANGED
|
@@ -27,7 +27,10 @@ export class SiteId {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
static generate(): SiteId {
|
|
30
|
-
const
|
|
30
|
+
const uniqueId = typeof crypto !== 'undefined' && crypto.randomUUID
|
|
31
|
+
? crypto.randomUUID()
|
|
32
|
+
: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
33
|
+
const id = `site-${uniqueId}`;
|
|
31
34
|
return new SiteId(id);
|
|
32
35
|
}
|
|
33
36
|
}
|
|
@@ -10,8 +10,6 @@ import { Session } from '../../tracking/aggregates/session.aggregate';
|
|
|
10
10
|
import { Event } from '../../tracking/entities/event.entity';
|
|
11
11
|
import { Pageview } from '../../tracking/entities/pageview.entity';
|
|
12
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
13
|
|
|
16
14
|
export interface TrackingCommandResult {
|
|
17
15
|
success: boolean;
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { Event } from '../entities/event.entity';
|
|
7
|
+
import type { Pageview } from '../entities/pageview.entity';
|
|
7
8
|
import type { EventId } from '../value-objects/event-id.vo';
|
|
8
9
|
import type { SessionId } from '../value-objects/session-id.vo';
|
|
9
10
|
|
|
10
11
|
export interface IEventRepository {
|
|
11
|
-
save(event: Event): Promise<void>;
|
|
12
|
+
save(event: Event | Pageview): Promise<void>;
|
|
12
13
|
findById(id: EventId): Promise<Event | null>;
|
|
13
14
|
findBySessionId(sessionId: SessionId): Promise<Event[]>;
|
|
14
15
|
delete(id: EventId): Promise<void>;
|
|
@@ -148,9 +148,12 @@ function parseOS(userAgent: string): OSInfo {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
function detectDeviceType(userAgent: string, screenWidth?: number): DeviceType {
|
|
151
|
+
const MOBILE_BREAKPOINT = 768;
|
|
152
|
+
const TABLET_BREAKPOINT = 1024;
|
|
153
|
+
|
|
151
154
|
if (screenWidth) {
|
|
152
|
-
if (screenWidth <
|
|
153
|
-
if (screenWidth <
|
|
155
|
+
if (screenWidth < MOBILE_BREAKPOINT) return 'mobile';
|
|
156
|
+
if (screenWidth < TABLET_BREAKPOINT) return 'tablet';
|
|
154
157
|
return 'desktop';
|
|
155
158
|
}
|
|
156
159
|
|
|
@@ -30,7 +30,10 @@ export class EventId {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
static generate(): EventId {
|
|
33
|
-
const
|
|
33
|
+
const uniqueId = typeof crypto !== 'undefined' && crypto.randomUUID
|
|
34
|
+
? crypto.randomUUID()
|
|
35
|
+
: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
36
|
+
const id = `event-${uniqueId}`;
|
|
34
37
|
return new EventId(id);
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -30,7 +30,10 @@ export class SessionId {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
static generate(): SessionId {
|
|
33
|
-
const
|
|
33
|
+
const uniqueId = typeof crypto !== 'undefined' && crypto.randomUUID
|
|
34
|
+
? crypto.randomUUID()
|
|
35
|
+
: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
36
|
+
const id = `session-${uniqueId}`;
|
|
34
37
|
return new SessionId(id);
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -32,13 +32,11 @@ export class HTTPAnalyticsRepository implements IAnalyticsRepository {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
if (!response.ok) {
|
|
35
|
-
console.error('Analytics fetch failed:', response.statusText);
|
|
36
35
|
return null;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
return await response.json();
|
|
40
|
-
} catch
|
|
41
|
-
console.error('Analytics error:', error);
|
|
39
|
+
} catch {
|
|
42
40
|
return null;
|
|
43
41
|
}
|
|
44
42
|
}
|
|
@@ -24,17 +24,23 @@ export class HTTPEventRepository implements IEventRepository {
|
|
|
24
24
|
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
25
25
|
private readonly FLUSH_INTERVAL = 30000; // 30 seconds
|
|
26
26
|
private readonly MAX_QUEUE_SIZE = 100;
|
|
27
|
+
private readonly FLUSH_THRESHOLD = 10; // Flush when queue reaches this size
|
|
28
|
+
private beforeUnloadHandler: (() => void) | null = null;
|
|
29
|
+
private isFlushing = false;
|
|
27
30
|
|
|
28
31
|
constructor(private readonly config: HTTPRepositoryConfig) {
|
|
29
32
|
this.startFlushTimer();
|
|
30
33
|
if (typeof window !== 'undefined') {
|
|
31
|
-
|
|
34
|
+
this.beforeUnloadHandler = () => {
|
|
35
|
+
void this.flush();
|
|
36
|
+
};
|
|
37
|
+
window.addEventListener('beforeunload', this.beforeUnloadHandler);
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
async save(event: Event): Promise<void> {
|
|
36
42
|
this.queue.push({ event, timestamp: Date.now() });
|
|
37
|
-
if (this.queue.length >=
|
|
43
|
+
if (this.queue.length >= this.FLUSH_THRESHOLD) {
|
|
38
44
|
await this.flush();
|
|
39
45
|
}
|
|
40
46
|
}
|
|
@@ -60,8 +66,11 @@ export class HTTPEventRepository implements IEventRepository {
|
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
private async flush(): Promise<void> {
|
|
63
|
-
if (this.queue.length === 0)
|
|
69
|
+
if (this.queue.length === 0 || this.isFlushing) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
64
72
|
|
|
73
|
+
this.isFlushing = true;
|
|
65
74
|
const items = [...this.queue];
|
|
66
75
|
this.queue = [];
|
|
67
76
|
|
|
@@ -79,20 +88,25 @@ export class HTTPEventRepository implements IEventRepository {
|
|
|
79
88
|
});
|
|
80
89
|
|
|
81
90
|
if (!response.ok) {
|
|
82
|
-
console.error('Tracking failed:', response.statusText);
|
|
83
91
|
this.queue.unshift(...items);
|
|
84
92
|
}
|
|
85
|
-
} catch
|
|
86
|
-
console.error('Tracking error:', error);
|
|
93
|
+
} catch {
|
|
87
94
|
this.queue.unshift(...items);
|
|
95
|
+
} finally {
|
|
96
|
+
this.isFlushing = false;
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
destroy(): void {
|
|
92
101
|
if (this.flushTimer) {
|
|
93
102
|
clearInterval(this.flushTimer);
|
|
103
|
+
this.flushTimer = null;
|
|
104
|
+
}
|
|
105
|
+
if (this.beforeUnloadHandler && typeof window !== 'undefined') {
|
|
106
|
+
window.removeEventListener('beforeunload', this.beforeUnloadHandler);
|
|
107
|
+
this.beforeUnloadHandler = null;
|
|
94
108
|
}
|
|
95
|
-
this.flush();
|
|
109
|
+
void this.flush();
|
|
96
110
|
}
|
|
97
111
|
}
|
|
98
112
|
|
|
@@ -104,7 +118,7 @@ export class HTTPPageviewRepository implements IPageviewRepository {
|
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
async save(pageview: Pageview): Promise<void> {
|
|
107
|
-
await this.eventRepo.save(pageview
|
|
121
|
+
await this.eventRepo.save(pageview);
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
async findById(id: EventId): Promise<Pageview | null> {
|
|
@@ -126,7 +140,11 @@ export class LocalSessionRepository implements ISessionRepository {
|
|
|
126
140
|
async save(session: Session): Promise<void> {
|
|
127
141
|
this.sessions.set(session.id.toString(), session);
|
|
128
142
|
if (typeof window !== 'undefined') {
|
|
129
|
-
|
|
143
|
+
try {
|
|
144
|
+
localStorage.setItem('wt_session', JSON.stringify(session.toJSON()));
|
|
145
|
+
} catch {
|
|
146
|
+
// Storage unavailable - continue without localStorage persistence
|
|
147
|
+
}
|
|
130
148
|
}
|
|
131
149
|
}
|
|
132
150
|
|
|
@@ -137,15 +155,19 @@ export class LocalSessionRepository implements ISessionRepository {
|
|
|
137
155
|
async findActive(deviceId: string, timeoutMs: number): Promise<Session | null> {
|
|
138
156
|
if (typeof window === 'undefined') return null;
|
|
139
157
|
|
|
140
|
-
|
|
141
|
-
|
|
158
|
+
try {
|
|
159
|
+
const stored = localStorage.getItem('wt_session');
|
|
160
|
+
if (!stored) return null;
|
|
142
161
|
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
const data = JSON.parse(stored);
|
|
163
|
+
const sessionId = new SessionId(data.id);
|
|
145
164
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
165
|
+
const session = this.sessions.get(sessionId.toString());
|
|
166
|
+
if (session && session.isActive(timeoutMs)) {
|
|
167
|
+
return session;
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Storage unavailable or corrupted data
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
return null;
|
|
@@ -154,7 +176,11 @@ export class LocalSessionRepository implements ISessionRepository {
|
|
|
154
176
|
async delete(id: SessionId): Promise<void> {
|
|
155
177
|
this.sessions.delete(id.toString());
|
|
156
178
|
if (typeof window !== 'undefined') {
|
|
157
|
-
|
|
179
|
+
try {
|
|
180
|
+
localStorage.removeItem('wt_session');
|
|
181
|
+
} catch {
|
|
182
|
+
// Storage unavailable - continue without cleanup
|
|
183
|
+
}
|
|
158
184
|
}
|
|
159
185
|
}
|
|
160
186
|
}
|
|
@@ -32,10 +32,12 @@ class WebTrafficService {
|
|
|
32
32
|
private eventRepo: HTTPEventRepository | null = null;
|
|
33
33
|
private pageviewRepo: HTTPPageviewRepository | null = null;
|
|
34
34
|
private sessionRepo: LocalSessionRepository | null = null;
|
|
35
|
+
private originalPushState: typeof history.pushState | null = null;
|
|
36
|
+
private originalReplaceState: typeof history.replaceState | null = null;
|
|
37
|
+
private popStateHandler: (() => void) | null = null;
|
|
35
38
|
|
|
36
39
|
initialize(config: WebTrafficConfig): void {
|
|
37
40
|
if (this.initialized) {
|
|
38
|
-
console.warn('WebTrafficService already initialized');
|
|
39
41
|
return;
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -53,8 +55,9 @@ class WebTrafficService {
|
|
|
53
55
|
this.pageviewRepo
|
|
54
56
|
);
|
|
55
57
|
|
|
56
|
-
// Get or create session
|
|
57
|
-
|
|
58
|
+
// Get or create session - this is async but we don't await it
|
|
59
|
+
// The service will return "not initialized" errors if called before session is ready
|
|
60
|
+
void this.initializeSession();
|
|
58
61
|
|
|
59
62
|
// Setup auto-tracking if enabled
|
|
60
63
|
if (config.autoTrack && typeof window !== 'undefined') {
|
|
@@ -129,13 +132,23 @@ class WebTrafficService {
|
|
|
129
132
|
private getOrCreateDeviceId(): string {
|
|
130
133
|
if (typeof window === 'undefined') return '';
|
|
131
134
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
try {
|
|
136
|
+
let deviceId = localStorage.getItem('wt_device_id');
|
|
137
|
+
if (!deviceId) {
|
|
138
|
+
const uniqueId = typeof crypto !== 'undefined' && crypto.randomUUID
|
|
139
|
+
? crypto.randomUUID()
|
|
140
|
+
: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
141
|
+
deviceId = `device-${uniqueId}`;
|
|
142
|
+
localStorage.setItem('wt_device_id', deviceId);
|
|
143
|
+
}
|
|
144
|
+
return deviceId;
|
|
145
|
+
} catch {
|
|
146
|
+
// Storage unavailable - generate temporary device ID
|
|
147
|
+
const uniqueId = typeof crypto !== 'undefined' && crypto.randomUUID
|
|
148
|
+
? crypto.randomUUID()
|
|
149
|
+
: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
150
|
+
return `device-${uniqueId}`;
|
|
136
151
|
}
|
|
137
|
-
|
|
138
|
-
return deviceId;
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
private getUTMFromURL() {
|
|
@@ -159,30 +172,52 @@ class WebTrafficService {
|
|
|
159
172
|
if (typeof window === 'undefined') return;
|
|
160
173
|
|
|
161
174
|
// Track initial pageview
|
|
162
|
-
this.trackPageView();
|
|
175
|
+
void this.trackPageView();
|
|
163
176
|
|
|
164
177
|
// Track SPA navigation
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
this.originalPushState = history.pushState;
|
|
179
|
+
this.originalReplaceState = history.replaceState;
|
|
167
180
|
|
|
168
181
|
history.pushState = (...args) => {
|
|
169
|
-
originalPushState
|
|
170
|
-
this.trackPageView();
|
|
182
|
+
this.originalPushState!.apply(history, args);
|
|
183
|
+
void this.trackPageView();
|
|
171
184
|
};
|
|
172
185
|
|
|
173
186
|
history.replaceState = (...args) => {
|
|
174
|
-
originalReplaceState
|
|
175
|
-
this.trackPageView();
|
|
187
|
+
this.originalReplaceState!.apply(history, args);
|
|
188
|
+
void this.trackPageView();
|
|
176
189
|
};
|
|
177
190
|
|
|
178
|
-
|
|
179
|
-
this.trackPageView();
|
|
180
|
-
}
|
|
191
|
+
this.popStateHandler = () => {
|
|
192
|
+
void this.trackPageView();
|
|
193
|
+
};
|
|
194
|
+
window.addEventListener('popstate', this.popStateHandler);
|
|
181
195
|
}
|
|
182
196
|
|
|
183
197
|
destroy(): void {
|
|
198
|
+
// Restore original history methods
|
|
199
|
+
if (this.originalPushState && typeof history !== 'undefined') {
|
|
200
|
+
history.pushState = this.originalPushState;
|
|
201
|
+
this.originalPushState = null;
|
|
202
|
+
}
|
|
203
|
+
if (this.originalReplaceState && typeof history !== 'undefined') {
|
|
204
|
+
history.replaceState = this.originalReplaceState;
|
|
205
|
+
this.originalReplaceState = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Remove popstate event listener
|
|
209
|
+
if (this.popStateHandler && typeof window !== 'undefined') {
|
|
210
|
+
window.removeEventListener('popstate', this.popStateHandler);
|
|
211
|
+
this.popStateHandler = null;
|
|
212
|
+
}
|
|
213
|
+
|
|
184
214
|
this.eventRepo?.destroy();
|
|
185
215
|
this.initialized = false;
|
|
216
|
+
this.commandService = null;
|
|
217
|
+
this.currentSession = null;
|
|
218
|
+
this.eventRepo = null;
|
|
219
|
+
this.pageviewRepo = null;
|
|
220
|
+
this.sessionRepo = null;
|
|
186
221
|
}
|
|
187
222
|
}
|
|
188
223
|
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import React, { createContext, useContext, useEffect, useMemo, type ReactNode } from 'react';
|
|
7
7
|
import { webTrafficService, type WebTrafficConfig } from '../infrastructure/tracking/web-traffic.service';
|
|
8
|
-
import type { WebTrafficContextValue
|
|
8
|
+
import type { WebTrafficContextValue } from './hooks';
|
|
9
|
+
import type { TrackingCommandResult } from '../domains/tracking/application/tracking-command.service';
|
|
9
10
|
|
|
10
11
|
const TrackingContext = createContext<WebTrafficContextValue | null>(null);
|
|
11
12
|
|
|
@@ -9,13 +9,7 @@ import { webTrafficService } from '../infrastructure/tracking/web-traffic.servic
|
|
|
9
9
|
import { HTTPAnalyticsRepository } from '../infrastructure/analytics/http-analytics.repository.impl';
|
|
10
10
|
import type { AnalyticsQuery } from '../domains/analytics/repositories/analytics.repository.interface';
|
|
11
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
|
-
|
|
12
|
+
import type { TrackingCommandResult } from '../domains/tracking/application/tracking-command.service';
|
|
19
13
|
import type { WebTrafficConfig } from '../infrastructure/tracking/web-traffic.service';
|
|
20
14
|
|
|
21
15
|
export interface WebTrafficContextValue {
|
|
@@ -29,17 +23,7 @@ export function useWebTraffic(): WebTrafficContextValue {
|
|
|
29
23
|
const context = useContext(WebTrafficContext);
|
|
30
24
|
|
|
31
25
|
if (!context) {
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
trackEvent: useCallback(async (name: string, properties?: Record<string, unknown>) => {
|
|
35
|
-
return webTrafficService.trackEvent(name, properties);
|
|
36
|
-
}, []),
|
|
37
|
-
trackPageView: useCallback(async (path?: string) => {
|
|
38
|
-
return webTrafficService.trackPageView(path);
|
|
39
|
-
}, []),
|
|
40
|
-
isInitialized: webTrafficService.isInitialized(),
|
|
41
|
-
config: { apiKey: '' }, // Fallback - context should always be used
|
|
42
|
-
};
|
|
26
|
+
throw new Error('useWebTraffic must be used within WebTrafficProvider');
|
|
43
27
|
}
|
|
44
28
|
|
|
45
29
|
return context;
|