@umituz/web-traffic 1.0.10 → 1.0.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-traffic",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Web analytics tracking library. Event tracking, pageviews, sessions, device info, and UTM parameter support.",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -27,7 +27,10 @@ export class SiteId {
27
27
  }
28
28
 
29
29
  static generate(): SiteId {
30
- const id = `site-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
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 < 768) return 'mobile';
153
- if (screenWidth < 1024) return 'tablet';
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 id = `event-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
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 id = `session-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
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 (error) {
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
- window.addEventListener('beforeunload', () => this.flush());
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 >= 10) {
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) return;
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 (error) {
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 as any);
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
- localStorage.setItem('wt_session', JSON.stringify(session.toJSON()));
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
- const stored = localStorage.getItem('wt_session');
141
- if (!stored) return null;
158
+ try {
159
+ const stored = localStorage.getItem('wt_session');
160
+ if (!stored) return null;
142
161
 
143
- const data = JSON.parse(stored);
144
- const sessionId = new SessionId(data.id);
162
+ const data = JSON.parse(stored);
163
+ const sessionId = new SessionId(data.id);
145
164
 
146
- const session = this.sessions.get(sessionId.toString());
147
- if (session && session.isActive(timeoutMs)) {
148
- return session;
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
- localStorage.removeItem('wt_session');
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
- this.initializeSession();
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
- let deviceId = localStorage.getItem('wt_device_id');
133
- if (!deviceId) {
134
- deviceId = `device-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
135
- localStorage.setItem('wt_device_id', deviceId);
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
- const originalPushState = history.pushState;
166
- const originalReplaceState = history.replaceState;
178
+ this.originalPushState = history.pushState;
179
+ this.originalReplaceState = history.replaceState;
167
180
 
168
181
  history.pushState = (...args) => {
169
- originalPushState.apply(history, args);
170
- this.trackPageView();
182
+ this.originalPushState!.apply(history, args);
183
+ void this.trackPageView();
171
184
  };
172
185
 
173
186
  history.replaceState = (...args) => {
174
- originalReplaceState.apply(history, args);
175
- this.trackPageView();
187
+ this.originalReplaceState!.apply(history, args);
188
+ void this.trackPageView();
176
189
  };
177
190
 
178
- window.addEventListener('popstate', () => {
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, TrackingCommandResult } from './hooks';
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
- // If no context, use service directly
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;