astro-tractstack 2.0.0-rc.26 → 2.0.0-rc.27
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
|
@@ -20,88 +20,115 @@ interface FetchAnalyticsProps {
|
|
|
20
20
|
onAnalyticsUpdate: (analytics: AnalyticsState) => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
23
|
+
// Global singleton state to prevent multi-component conflicts
|
|
24
|
+
class AnalyticsService {
|
|
25
|
+
private static instance: AnalyticsService;
|
|
26
|
+
private isInitialized = false;
|
|
27
|
+
private activeRequest: AbortController | null = null;
|
|
28
|
+
private requestCache = new Map<string, { data: any; timestamp: number }>();
|
|
29
|
+
private readonly CACHE_TTL = 5000; // 5 seconds
|
|
30
|
+
private readonly DEBOUNCE_MS = 300;
|
|
31
|
+
private debounceTimer: NodeJS.Timeout | null = null;
|
|
32
|
+
private floodProtection = {
|
|
33
|
+
requestCount: 0,
|
|
34
|
+
windowStart: 0,
|
|
35
|
+
isBlocked: false,
|
|
36
|
+
};
|
|
37
|
+
private readonly FLOOD_WINDOW_MS = 10000; // 10 seconds
|
|
38
|
+
private readonly FLOOD_THRESHOLD = 5;
|
|
39
|
+
|
|
40
|
+
static getInstance(): AnalyticsService {
|
|
41
|
+
if (!AnalyticsService.instance) {
|
|
42
|
+
AnalyticsService.instance = new AnalyticsService();
|
|
43
|
+
}
|
|
44
|
+
return AnalyticsService.instance;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private getCacheKey(params: URLSearchParams): string {
|
|
48
|
+
const sorted = new URLSearchParams([...params.entries()].sort());
|
|
49
|
+
return sorted.toString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private isFloodBlocked(): boolean {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
|
|
55
|
+
// Reset window if needed
|
|
56
|
+
if (now - this.floodProtection.windowStart > this.FLOOD_WINDOW_MS) {
|
|
57
|
+
this.floodProtection.requestCount = 0;
|
|
58
|
+
this.floodProtection.windowStart = now;
|
|
59
|
+
this.floodProtection.isBlocked = false;
|
|
60
|
+
}
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
// Check if blocked
|
|
63
|
+
if (this.floodProtection.isBlocked) {
|
|
64
|
+
if (VERBOSE) console.log('🚫 Request blocked by flood protection');
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Increment counter and check threshold
|
|
69
|
+
this.floodProtection.requestCount++;
|
|
70
|
+
if (this.floodProtection.requestCount > this.FLOOD_THRESHOLD) {
|
|
71
|
+
this.floodProtection.isBlocked = true;
|
|
72
|
+
if (VERBOSE) console.log('🚨 Flood protection activated');
|
|
73
|
+
|
|
74
|
+
// Auto-unblock after delay
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
this.floodProtection.isBlocked = false;
|
|
77
|
+
if (VERBOSE) console.log('✅ Flood protection deactivated');
|
|
78
|
+
}, 30000); // 30 second cooldown
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private getCachedResponse(cacheKey: string): any | null {
|
|
87
|
+
const cached = this.requestCache.get(cacheKey);
|
|
88
|
+
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
|
|
89
|
+
if (VERBOSE) console.log('📦 Using cached response');
|
|
90
|
+
return cached.data;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private setCachedResponse(cacheKey: string, data: any): void {
|
|
96
|
+
this.requestCache.set(cacheKey, { data, timestamp: Date.now() });
|
|
97
|
+
|
|
98
|
+
// Cleanup old cache entries
|
|
99
|
+
const cutoff = Date.now() - this.CACHE_TTL;
|
|
100
|
+
for (const [key, value] of this.requestCache.entries()) {
|
|
101
|
+
if (value.timestamp < cutoff) {
|
|
102
|
+
this.requestCache.delete(key);
|
|
57
103
|
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
visitorType: $epinetCustomFilters.visitorType,
|
|
70
|
-
selectedUserId: $epinetCustomFilters.selectedUserId,
|
|
71
|
-
},
|
|
72
|
-
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async fetchAnalytics(
|
|
108
|
+
filters: any,
|
|
109
|
+
onUpdate: (data: AnalyticsState) => void
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
// Flood protection
|
|
112
|
+
if (this.isFloodBlocked()) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
73
115
|
|
|
74
116
|
try {
|
|
75
|
-
//
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
117
|
+
// Cancel any existing request
|
|
118
|
+
if (this.activeRequest) {
|
|
119
|
+
this.activeRequest.abort();
|
|
120
|
+
if (VERBOSE) console.log('🛑 Cancelled previous request');
|
|
79
121
|
}
|
|
80
122
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
onAnalyticsUpdate({
|
|
84
|
-
dashboard: null,
|
|
85
|
-
leads: null,
|
|
86
|
-
epinet: null,
|
|
87
|
-
userCounts: [],
|
|
88
|
-
hourlyNodeActivity: {},
|
|
89
|
-
isLoading: true,
|
|
90
|
-
status: 'loading',
|
|
91
|
-
error: null,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const { startTimeUTC, endTimeUTC, visitorType, selectedUserId } =
|
|
95
|
-
$epinetCustomFilters;
|
|
123
|
+
// Create new abort controller
|
|
124
|
+
this.activeRequest = new AbortController();
|
|
96
125
|
|
|
97
|
-
// Build URL parameters
|
|
126
|
+
// Build URL parameters
|
|
98
127
|
const params = new URLSearchParams();
|
|
99
|
-
|
|
100
|
-
if (startTimeUTC && endTimeUTC) {
|
|
101
|
-
// Convert UTC timestamps to hours-back integers (what backend expects)
|
|
128
|
+
if (filters.startTimeUTC && filters.endTimeUTC) {
|
|
102
129
|
const now = new Date();
|
|
103
|
-
const startTime = new Date(startTimeUTC);
|
|
104
|
-
const endTime = new Date(endTimeUTC);
|
|
130
|
+
const startTime = new Date(filters.startTimeUTC);
|
|
131
|
+
const endTime = new Date(filters.endTimeUTC);
|
|
105
132
|
|
|
106
133
|
const startHour = Math.ceil(
|
|
107
134
|
(now.getTime() - startTime.getTime()) / (1000 * 60 * 60)
|
|
@@ -112,27 +139,42 @@ export default function FetchAnalytics({
|
|
|
112
139
|
|
|
113
140
|
params.append('startHour', startHour.toString());
|
|
114
141
|
params.append('endHour', endHour.toString());
|
|
142
|
+
}
|
|
115
143
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
if (filters.visitorType)
|
|
145
|
+
params.append('visitorType', filters.visitorType);
|
|
146
|
+
if (filters.selectedUserId)
|
|
147
|
+
params.append('userId', filters.selectedUserId);
|
|
148
|
+
|
|
149
|
+
const cacheKey = this.getCacheKey(params);
|
|
150
|
+
|
|
151
|
+
// Check cache first
|
|
152
|
+
const cachedData = this.getCachedResponse(cacheKey);
|
|
153
|
+
if (cachedData) {
|
|
154
|
+
onUpdate(cachedData);
|
|
155
|
+
return;
|
|
124
156
|
}
|
|
125
157
|
|
|
126
|
-
|
|
127
|
-
|
|
158
|
+
// Set loading state
|
|
159
|
+
onUpdate({
|
|
160
|
+
dashboard: null,
|
|
161
|
+
leads: null,
|
|
162
|
+
epinet: null,
|
|
163
|
+
userCounts: [],
|
|
164
|
+
hourlyNodeActivity: {},
|
|
165
|
+
isLoading: true,
|
|
166
|
+
status: 'loading',
|
|
167
|
+
error: null,
|
|
168
|
+
});
|
|
128
169
|
|
|
129
|
-
//
|
|
170
|
+
// Make request using existing TractStackAPI
|
|
130
171
|
const api = new TractStackAPI(
|
|
131
172
|
window.TRACTSTACK_CONFIG?.tenantId || 'default'
|
|
132
173
|
);
|
|
133
174
|
const endpoint = `/api/v1/analytics/all${params.toString() ? `?${params.toString()}` : ''}`;
|
|
134
175
|
|
|
135
|
-
if (VERBOSE) console.log('
|
|
176
|
+
if (VERBOSE) console.log('🔥 Making API request', { endpoint });
|
|
177
|
+
|
|
136
178
|
const response = await api.get(endpoint);
|
|
137
179
|
|
|
138
180
|
if (!response.success) {
|
|
@@ -140,15 +182,8 @@ export default function FetchAnalytics({
|
|
|
140
182
|
}
|
|
141
183
|
|
|
142
184
|
const data = response.data;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
hasData: !!data,
|
|
146
|
-
dataKeys: data ? Object.keys(data) : [],
|
|
147
|
-
userCountsLength: data?.userCounts?.length || 0,
|
|
148
|
-
hasHourlyNodeActivity: !!data?.hourlyNodeActivity,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Check if data is still loading - add polling logic here
|
|
185
|
+
|
|
186
|
+
// Check if data is still loading - implement polling logic
|
|
152
187
|
const isStillLoading =
|
|
153
188
|
data?.status === 'loading' ||
|
|
154
189
|
data?.status === 'refreshing' ||
|
|
@@ -159,63 +194,42 @@ export default function FetchAnalytics({
|
|
|
159
194
|
data?.epinet?.status === 'loading' ||
|
|
160
195
|
data?.epinet?.status === 'refreshing';
|
|
161
196
|
|
|
162
|
-
if (
|
|
163
|
-
console.log('
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const delayMs =
|
|
192
|
-
POLLING_DELAYS[pollingAttempts] ||
|
|
193
|
-
POLLING_DELAYS[POLLING_DELAYS.length - 1];
|
|
194
|
-
|
|
195
|
-
const newTimer = setTimeout(() => {
|
|
196
|
-
setPollingAttempts(pollingAttempts + 1);
|
|
197
|
-
fetchAllAnalytics();
|
|
198
|
-
}, delayMs);
|
|
199
|
-
|
|
200
|
-
setPollingTimer(newTimer);
|
|
201
|
-
|
|
202
|
-
// Update with partial data but keep loading state
|
|
203
|
-
onAnalyticsUpdate({
|
|
204
|
-
dashboard: data.dashboard,
|
|
205
|
-
leads: data.leads,
|
|
206
|
-
epinet: data.epinet,
|
|
207
|
-
userCounts: data.userCounts || [],
|
|
208
|
-
hourlyNodeActivity: data.hourlyNodeActivity || {},
|
|
209
|
-
status: 'loading',
|
|
210
|
-
error: null,
|
|
211
|
-
isLoading: true,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
return;
|
|
197
|
+
if (isStillLoading) {
|
|
198
|
+
if (VERBOSE) console.log('⏳ Backend data still loading, will poll...');
|
|
199
|
+
|
|
200
|
+
// Update with partial data but keep loading state
|
|
201
|
+
const partialAnalytics = {
|
|
202
|
+
dashboard: data.dashboard,
|
|
203
|
+
leads: data.leads,
|
|
204
|
+
epinet: data.epinet,
|
|
205
|
+
userCounts: data.userCounts || [],
|
|
206
|
+
hourlyNodeActivity: data.hourlyNodeActivity || {},
|
|
207
|
+
status: 'loading',
|
|
208
|
+
error: null,
|
|
209
|
+
isLoading: true,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
onUpdate(partialAnalytics);
|
|
213
|
+
|
|
214
|
+
// Schedule polling retry - use the cache key to prevent multiple polls
|
|
215
|
+
const pollKey = `poll_${cacheKey}`;
|
|
216
|
+
if (!this.requestCache.has(pollKey)) {
|
|
217
|
+
this.requestCache.set(pollKey, { data: null, timestamp: Date.now() });
|
|
218
|
+
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
this.requestCache.delete(pollKey);
|
|
221
|
+
// Clear the main cache entry to force fresh request
|
|
222
|
+
this.requestCache.delete(cacheKey);
|
|
223
|
+
// Retry the fetch
|
|
224
|
+
this.fetchAnalytics(filters, onUpdate);
|
|
225
|
+
}, 2000);
|
|
215
226
|
}
|
|
227
|
+
|
|
228
|
+
return;
|
|
216
229
|
}
|
|
217
230
|
|
|
218
|
-
|
|
231
|
+
// Process successful response
|
|
232
|
+
const analyticsData = {
|
|
219
233
|
dashboard: data.dashboard,
|
|
220
234
|
leads: data.leads,
|
|
221
235
|
epinet: data.epinet,
|
|
@@ -226,90 +240,62 @@ export default function FetchAnalytics({
|
|
|
226
240
|
isLoading: false,
|
|
227
241
|
};
|
|
228
242
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// Update
|
|
233
|
-
|
|
234
|
-
console.log('🔄 BEFORE store update', {
|
|
235
|
-
currentStoreRef: epinetCustomFilters.get(),
|
|
236
|
-
aboutToSet: {
|
|
237
|
-
userCounts: data.userCounts?.length || 0,
|
|
238
|
-
hourlyNodeActivity: !!data.hourlyNodeActivity,
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
|
|
243
|
-
...$epinetCustomFilters,
|
|
244
|
-
userCounts: data.userCounts || [],
|
|
245
|
-
hourlyNodeActivity: data.hourlyNodeActivity || {},
|
|
246
|
-
});
|
|
243
|
+
// Cache the response
|
|
244
|
+
this.setCachedResponse(cacheKey, analyticsData);
|
|
245
|
+
|
|
246
|
+
// Update caller
|
|
247
|
+
onUpdate(analyticsData);
|
|
247
248
|
|
|
248
|
-
|
|
249
|
-
setPollingAttempts(0);
|
|
250
|
-
setPollingStartTime(null);
|
|
249
|
+
if (VERBOSE) console.log('✅ Analytics request completed successfully');
|
|
251
250
|
|
|
252
|
-
|
|
253
|
-
console.log('🔄 AFTER store update', {
|
|
254
|
-
newStoreRef: epinetCustomFilters.get(),
|
|
255
|
-
});
|
|
251
|
+
this.activeRequest = null;
|
|
256
252
|
} catch (error) {
|
|
257
|
-
|
|
253
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
254
|
+
if (VERBOSE) console.log('🔄 Request aborted');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
258
257
|
|
|
259
|
-
|
|
260
|
-
error instanceof Error ? error.message : 'Unknown error occurred';
|
|
258
|
+
console.error('❌ Analytics fetch error:', error);
|
|
261
259
|
|
|
262
|
-
|
|
260
|
+
onUpdate({
|
|
263
261
|
dashboard: null,
|
|
264
262
|
leads: null,
|
|
265
263
|
epinet: null,
|
|
266
264
|
userCounts: [],
|
|
267
265
|
hourlyNodeActivity: {},
|
|
268
266
|
status: 'error',
|
|
269
|
-
error:
|
|
267
|
+
error:
|
|
268
|
+
error instanceof Error ? error.message : 'Unknown error occurred',
|
|
270
269
|
isLoading: false,
|
|
271
270
|
});
|
|
272
271
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
console.log(
|
|
277
|
-
'🔄 Scheduling retry due to error, attempt',
|
|
278
|
-
pollingAttempts + 1
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
const delayMs =
|
|
282
|
-
POLLING_DELAYS[pollingAttempts] ||
|
|
283
|
-
POLLING_DELAYS[POLLING_DELAYS.length - 1];
|
|
284
|
-
|
|
285
|
-
const newTimer = setTimeout(() => {
|
|
286
|
-
setPollingAttempts(pollingAttempts + 1);
|
|
287
|
-
fetchAllAnalytics();
|
|
288
|
-
}, delayMs);
|
|
272
|
+
this.activeRequest = null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
289
275
|
|
|
290
|
-
|
|
291
|
-
|
|
276
|
+
cleanup(): void {
|
|
277
|
+
if (this.activeRequest) {
|
|
278
|
+
this.activeRequest.abort();
|
|
279
|
+
this.activeRequest = null;
|
|
292
280
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
pollingAttempts,
|
|
299
|
-
]);
|
|
281
|
+
if (this.debounceTimer) {
|
|
282
|
+
clearTimeout(this.debounceTimer);
|
|
283
|
+
this.debounceTimer = null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
300
286
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (!isInitialized.current && !isInitializing.current) {
|
|
304
|
-
if (VERBOSE) console.log('🏁 Initializing FetchAnalytics');
|
|
305
|
-
isInitializing.current = true;
|
|
287
|
+
initializeFilters(tenantId: string): void {
|
|
288
|
+
if (this.isInitialized) return;
|
|
306
289
|
|
|
307
|
-
|
|
308
|
-
const oneWeekAgoUTC = new Date(
|
|
309
|
-
nowUTC.getTime() - 7 * 24 * 60 * 60 * 1000
|
|
310
|
-
);
|
|
290
|
+
if (VERBOSE) console.log('🏁 Initializing analytics filters');
|
|
311
291
|
|
|
312
|
-
|
|
292
|
+
const nowUTC = new Date();
|
|
293
|
+
const oneWeekAgoUTC = new Date(nowUTC.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
294
|
+
|
|
295
|
+
// Only set if not already initialized to prevent store churn
|
|
296
|
+
const current = epinetCustomFilters.get();
|
|
297
|
+
if (!current.enabled) {
|
|
298
|
+
epinetCustomFilters.set(tenantId, {
|
|
313
299
|
enabled: true,
|
|
314
300
|
visitorType: 'all',
|
|
315
301
|
selectedUserId: null,
|
|
@@ -318,32 +304,94 @@ export default function FetchAnalytics({
|
|
|
318
304
|
userCounts: [],
|
|
319
305
|
hourlyNodeActivity: {},
|
|
320
306
|
});
|
|
307
|
+
}
|
|
321
308
|
|
|
322
|
-
|
|
323
|
-
|
|
309
|
+
this.isInitialized = true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
debouncedFetch(filters: any, onUpdate: (data: AnalyticsState) => void): void {
|
|
313
|
+
// Clear existing debounce timer
|
|
314
|
+
if (this.debounceTimer) {
|
|
315
|
+
clearTimeout(this.debounceTimer);
|
|
324
316
|
}
|
|
317
|
+
|
|
318
|
+
// Set new debounced fetch
|
|
319
|
+
this.debounceTimer = setTimeout(() => {
|
|
320
|
+
this.fetchAnalytics(filters, onUpdate);
|
|
321
|
+
}, this.DEBOUNCE_MS);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export default function FetchAnalytics({
|
|
326
|
+
onAnalyticsUpdate,
|
|
327
|
+
}: FetchAnalyticsProps) {
|
|
328
|
+
const $epinetCustomFilters = useStore(epinetCustomFilters);
|
|
329
|
+
const analyticsService = useRef(AnalyticsService.getInstance());
|
|
330
|
+
const lastFiltersRef = useRef<string>('');
|
|
331
|
+
|
|
332
|
+
if (VERBOSE) {
|
|
333
|
+
console.log('🔄 FetchAnalytics render', {
|
|
334
|
+
filters: {
|
|
335
|
+
startTimeUTC: $epinetCustomFilters.startTimeUTC,
|
|
336
|
+
endTimeUTC: $epinetCustomFilters.endTimeUTC,
|
|
337
|
+
visitorType: $epinetCustomFilters.visitorType,
|
|
338
|
+
selectedUserId: $epinetCustomFilters.selectedUserId,
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Cleanup on unmount
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
return () => {
|
|
346
|
+
analyticsService.current.cleanup();
|
|
347
|
+
};
|
|
325
348
|
}, []);
|
|
326
349
|
|
|
327
|
-
//
|
|
350
|
+
// Initialize filters once
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
|
|
353
|
+
analyticsService.current.initializeFilters(tenantId);
|
|
354
|
+
}, []);
|
|
355
|
+
|
|
356
|
+
// Debounced fetch when filters change
|
|
328
357
|
useEffect(() => {
|
|
329
358
|
if (
|
|
330
|
-
|
|
331
|
-
$epinetCustomFilters.
|
|
332
|
-
$epinetCustomFilters.
|
|
333
|
-
$epinetCustomFilters.
|
|
334
|
-
$epinetCustomFilters.endTimeUTC !== null
|
|
359
|
+
!$epinetCustomFilters.enabled ||
|
|
360
|
+
$epinetCustomFilters.visitorType === null ||
|
|
361
|
+
$epinetCustomFilters.startTimeUTC === null ||
|
|
362
|
+
$epinetCustomFilters.endTimeUTC === null
|
|
335
363
|
) {
|
|
336
|
-
|
|
337
|
-
setPollingAttempts(0); // Reset polling attempts when filters change
|
|
338
|
-
setPollingStartTime(null); // Reset polling start time
|
|
339
|
-
fetchAllAnalytics();
|
|
364
|
+
return;
|
|
340
365
|
}
|
|
366
|
+
|
|
367
|
+
// Create stable filter signature to prevent unnecessary fetches
|
|
368
|
+
const filtersSignature = JSON.stringify({
|
|
369
|
+
startTimeUTC: $epinetCustomFilters.startTimeUTC,
|
|
370
|
+
endTimeUTC: $epinetCustomFilters.endTimeUTC,
|
|
371
|
+
visitorType: $epinetCustomFilters.visitorType,
|
|
372
|
+
selectedUserId: $epinetCustomFilters.selectedUserId,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Skip if filters haven't actually changed
|
|
376
|
+
if (filtersSignature === lastFiltersRef.current) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
lastFiltersRef.current = filtersSignature;
|
|
381
|
+
|
|
382
|
+
if (VERBOSE) console.log('🔄 Filters changed, debouncing fetch');
|
|
383
|
+
|
|
384
|
+
analyticsService.current.debouncedFetch(
|
|
385
|
+
$epinetCustomFilters,
|
|
386
|
+
onAnalyticsUpdate
|
|
387
|
+
);
|
|
341
388
|
}, [
|
|
342
389
|
$epinetCustomFilters.enabled,
|
|
343
390
|
$epinetCustomFilters.visitorType,
|
|
344
391
|
$epinetCustomFilters.selectedUserId,
|
|
345
392
|
$epinetCustomFilters.startTimeUTC,
|
|
346
393
|
$epinetCustomFilters.endTimeUTC,
|
|
394
|
+
onAnalyticsUpdate,
|
|
347
395
|
]);
|
|
348
396
|
|
|
349
397
|
return null;
|