astro-tractstack 2.0.0 → 2.0.2
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/templates/src/components/codehooks/EpinetDurationSelector.tsx +98 -179
- package/templates/src/components/codehooks/EpinetTableView.tsx +100 -75
- package/templates/src/components/codehooks/EpinetWrapper.tsx +71 -112
- package/templates/src/components/codehooks/SankeyDiagram.tsx +2 -2
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +37 -1
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +21 -70
- package/templates/src/stores/analytics.ts +14 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState,
|
|
1
|
+
import { useState, useCallback, useMemo, Component } from 'react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import { useStore } from '@nanostores/react';
|
|
4
4
|
import { epinetCustomFilters } from '@/stores/analytics';
|
|
@@ -104,6 +104,30 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
104
104
|
error: null,
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
const handleBeliefFilterChange = (beliefSlug: string, value: string) => {
|
|
108
|
+
const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
|
|
109
|
+
const currentFilters = epinetCustomFilters.get();
|
|
110
|
+
let newFilters = [...(currentFilters.appliedFilters || [])];
|
|
111
|
+
|
|
112
|
+
if (value === 'All') {
|
|
113
|
+
newFilters = newFilters.filter((f) => f.beliefSlug !== beliefSlug);
|
|
114
|
+
} else {
|
|
115
|
+
const existingIndex = newFilters.findIndex(
|
|
116
|
+
(f) => f.beliefSlug === beliefSlug
|
|
117
|
+
);
|
|
118
|
+
if (existingIndex > -1) {
|
|
119
|
+
newFilters[existingIndex] = { beliefSlug, value };
|
|
120
|
+
} else {
|
|
121
|
+
newFilters.push({ beliefSlug, value });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
epinetCustomFilters.set(tenantId, {
|
|
126
|
+
...currentFilters,
|
|
127
|
+
appliedFilters: newFilters,
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
107
131
|
// Duration helper for UI
|
|
108
132
|
const currentDurationHelper = useMemo(():
|
|
109
133
|
| 'daily'
|
|
@@ -440,6 +464,10 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
440
464
|
analytics.isLoading || analytics.status === 'loading'
|
|
441
465
|
}
|
|
442
466
|
hourlyNodeActivity={analytics.hourlyNodeActivity}
|
|
467
|
+
// MODIFICATION: Read availableFilters from the store, not local state
|
|
468
|
+
availableFilters={$epinetCustomFilters.availableFilters}
|
|
469
|
+
appliedFilters={$epinetCustomFilters.appliedFilters}
|
|
470
|
+
onBeliefFilterChange={handleBeliefFilterChange}
|
|
443
471
|
/>
|
|
444
472
|
</div>
|
|
445
473
|
</ErrorBoundary>
|
|
@@ -455,6 +483,10 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
455
483
|
analytics.isLoading || analytics.status === 'loading'
|
|
456
484
|
}
|
|
457
485
|
hourlyNodeActivity={analytics.hourlyNodeActivity}
|
|
486
|
+
// MODIFICATION: Read availableFilters from the store, not local state
|
|
487
|
+
availableFilters={$epinetCustomFilters.availableFilters}
|
|
488
|
+
appliedFilters={$epinetCustomFilters.appliedFilters}
|
|
489
|
+
onBeliefFilterChange={handleBeliefFilterChange}
|
|
458
490
|
/>
|
|
459
491
|
</>
|
|
460
492
|
)
|
|
@@ -468,6 +500,10 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
468
500
|
fullContentMap={fullContentMap}
|
|
469
501
|
isLoading={analytics.isLoading || analytics.status === 'loading'}
|
|
470
502
|
hourlyNodeActivity={analytics.hourlyNodeActivity}
|
|
503
|
+
// MODIFICATION: Read availableFilters from the store, not local state
|
|
504
|
+
availableFilters={$epinetCustomFilters.availableFilters}
|
|
505
|
+
appliedFilters={$epinetCustomFilters.appliedFilters}
|
|
506
|
+
onBeliefFilterChange={handleBeliefFilterChange}
|
|
471
507
|
/>
|
|
472
508
|
</>
|
|
473
509
|
)}
|
|
@@ -20,13 +20,12 @@ interface FetchAnalyticsProps {
|
|
|
20
20
|
onAnalyticsUpdate: (analytics: AnalyticsState) => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// Global singleton state to prevent multi-component conflicts
|
|
24
23
|
class AnalyticsService {
|
|
25
24
|
private static instance: AnalyticsService;
|
|
26
25
|
private isInitialized = false;
|
|
27
26
|
private activeRequest: AbortController | null = null;
|
|
28
27
|
private requestCache = new Map<string, { data: any; timestamp: number }>();
|
|
29
|
-
private readonly CACHE_TTL = 5000;
|
|
28
|
+
private readonly CACHE_TTL = 5000;
|
|
30
29
|
private readonly DEBOUNCE_MS = 300;
|
|
31
30
|
private debounceTimer: NodeJS.Timeout | null = null;
|
|
32
31
|
private floodProtection = {
|
|
@@ -34,7 +33,7 @@ class AnalyticsService {
|
|
|
34
33
|
windowStart: 0,
|
|
35
34
|
isBlocked: false,
|
|
36
35
|
};
|
|
37
|
-
private readonly FLOOD_WINDOW_MS = 10000;
|
|
36
|
+
private readonly FLOOD_WINDOW_MS = 10000;
|
|
38
37
|
private readonly FLOOD_THRESHOLD = 5;
|
|
39
38
|
|
|
40
39
|
static getInstance(): AnalyticsService {
|
|
@@ -51,35 +50,25 @@ class AnalyticsService {
|
|
|
51
50
|
|
|
52
51
|
private isFloodBlocked(): boolean {
|
|
53
52
|
const now = Date.now();
|
|
54
|
-
|
|
55
|
-
// Reset window if needed
|
|
56
53
|
if (now - this.floodProtection.windowStart > this.FLOOD_WINDOW_MS) {
|
|
57
54
|
this.floodProtection.requestCount = 0;
|
|
58
55
|
this.floodProtection.windowStart = now;
|
|
59
56
|
this.floodProtection.isBlocked = false;
|
|
60
57
|
}
|
|
61
|
-
|
|
62
|
-
// Check if blocked
|
|
63
58
|
if (this.floodProtection.isBlocked) {
|
|
64
59
|
if (VERBOSE) console.log('🚫 Request blocked by flood protection');
|
|
65
60
|
return true;
|
|
66
61
|
}
|
|
67
|
-
|
|
68
|
-
// Increment counter and check threshold
|
|
69
62
|
this.floodProtection.requestCount++;
|
|
70
63
|
if (this.floodProtection.requestCount > this.FLOOD_THRESHOLD) {
|
|
71
64
|
this.floodProtection.isBlocked = true;
|
|
72
65
|
if (VERBOSE) console.log('🚨 Flood protection activated');
|
|
73
|
-
|
|
74
|
-
// Auto-unblock after delay
|
|
75
66
|
setTimeout(() => {
|
|
76
67
|
this.floodProtection.isBlocked = false;
|
|
77
68
|
if (VERBOSE) console.log('✅ Flood protection deactivated');
|
|
78
|
-
}, 30000);
|
|
79
|
-
|
|
69
|
+
}, 30000);
|
|
80
70
|
return true;
|
|
81
71
|
}
|
|
82
|
-
|
|
83
72
|
return false;
|
|
84
73
|
}
|
|
85
74
|
|
|
@@ -94,8 +83,6 @@ class AnalyticsService {
|
|
|
94
83
|
|
|
95
84
|
private setCachedResponse(cacheKey: string, data: any): void {
|
|
96
85
|
this.requestCache.set(cacheKey, { data, timestamp: Date.now() });
|
|
97
|
-
|
|
98
|
-
// Cleanup old cache entries
|
|
99
86
|
const cutoff = Date.now() - this.CACHE_TTL;
|
|
100
87
|
for (const [key, value] of this.requestCache.entries()) {
|
|
101
88
|
if (value.timestamp < cutoff) {
|
|
@@ -108,35 +95,29 @@ class AnalyticsService {
|
|
|
108
95
|
filters: any,
|
|
109
96
|
onUpdate: (data: AnalyticsState) => void
|
|
110
97
|
): Promise<void> {
|
|
111
|
-
// Flood protection
|
|
112
98
|
if (this.isFloodBlocked()) {
|
|
113
99
|
return;
|
|
114
100
|
}
|
|
115
101
|
|
|
116
102
|
try {
|
|
117
|
-
// Cancel any existing request
|
|
118
103
|
if (this.activeRequest) {
|
|
119
104
|
this.activeRequest.abort();
|
|
120
105
|
if (VERBOSE) console.log('🛑 Cancelled previous request');
|
|
121
106
|
}
|
|
122
107
|
|
|
123
|
-
// Create new abort controller
|
|
124
108
|
this.activeRequest = new AbortController();
|
|
125
109
|
|
|
126
|
-
// Build URL parameters
|
|
127
110
|
const params = new URLSearchParams();
|
|
128
111
|
if (filters.startTimeUTC && filters.endTimeUTC) {
|
|
129
112
|
const now = new Date();
|
|
130
113
|
const startTime = new Date(filters.startTimeUTC);
|
|
131
114
|
const endTime = new Date(filters.endTimeUTC);
|
|
132
|
-
|
|
133
115
|
const startHour = Math.ceil(
|
|
134
116
|
(now.getTime() - startTime.getTime()) / (1000 * 60 * 60)
|
|
135
117
|
);
|
|
136
118
|
const endHour = Math.floor(
|
|
137
119
|
(now.getTime() - endTime.getTime()) / (1000 * 60 * 60)
|
|
138
120
|
);
|
|
139
|
-
|
|
140
121
|
params.append('startHour', startHour.toString());
|
|
141
122
|
params.append('endHour', endHour.toString());
|
|
142
123
|
}
|
|
@@ -146,16 +127,18 @@ class AnalyticsService {
|
|
|
146
127
|
if (filters.selectedUserId)
|
|
147
128
|
params.append('userId', filters.selectedUserId);
|
|
148
129
|
|
|
149
|
-
|
|
130
|
+
// MODIFICATION: Properly format appliedFilters for the backend
|
|
131
|
+
if (filters.appliedFilters && filters.appliedFilters.length > 0) {
|
|
132
|
+
params.append('appliedFilters', JSON.stringify(filters.appliedFilters));
|
|
133
|
+
}
|
|
150
134
|
|
|
151
|
-
|
|
135
|
+
const cacheKey = this.getCacheKey(params);
|
|
152
136
|
const cachedData = this.getCachedResponse(cacheKey);
|
|
153
137
|
if (cachedData) {
|
|
154
138
|
onUpdate(cachedData);
|
|
155
139
|
return;
|
|
156
140
|
}
|
|
157
141
|
|
|
158
|
-
// Set loading state
|
|
159
142
|
onUpdate({
|
|
160
143
|
dashboard: null,
|
|
161
144
|
leads: null,
|
|
@@ -167,23 +150,17 @@ class AnalyticsService {
|
|
|
167
150
|
error: null,
|
|
168
151
|
});
|
|
169
152
|
|
|
170
|
-
// Make request using existing TractStackAPI
|
|
171
153
|
const api = new TractStackAPI(
|
|
172
154
|
window.TRACTSTACK_CONFIG?.tenantId || 'default'
|
|
173
155
|
);
|
|
174
156
|
const endpoint = `/api/v1/analytics/all${params.toString() ? `?${params.toString()}` : ''}`;
|
|
175
|
-
|
|
176
157
|
if (VERBOSE) console.log('🔥 Making API request', { endpoint });
|
|
177
158
|
|
|
178
159
|
const response = await api.get(endpoint);
|
|
179
|
-
|
|
180
|
-
if (!response.success) {
|
|
160
|
+
if (!response.success)
|
|
181
161
|
throw new Error(response.error || 'Failed to fetch analytics data');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
162
|
const data = response.data;
|
|
185
163
|
|
|
186
|
-
// Check if data is still loading - implement polling logic
|
|
187
164
|
const isStillLoading =
|
|
188
165
|
data?.status === 'loading' ||
|
|
189
166
|
data?.status === 'refreshing' ||
|
|
@@ -196,8 +173,6 @@ class AnalyticsService {
|
|
|
196
173
|
|
|
197
174
|
if (isStillLoading) {
|
|
198
175
|
if (VERBOSE) console.log('⏳ Backend data still loading, will poll...');
|
|
199
|
-
|
|
200
|
-
// Update with partial data but keep loading state
|
|
201
176
|
const partialAnalytics = {
|
|
202
177
|
dashboard: data.dashboard,
|
|
203
178
|
leads: data.leads,
|
|
@@ -208,27 +183,20 @@ class AnalyticsService {
|
|
|
208
183
|
error: null,
|
|
209
184
|
isLoading: true,
|
|
210
185
|
};
|
|
211
|
-
|
|
212
186
|
onUpdate(partialAnalytics);
|
|
213
187
|
|
|
214
|
-
// Schedule polling retry - use the cache key to prevent multiple polls
|
|
215
188
|
const pollKey = `poll_${cacheKey}`;
|
|
216
189
|
if (!this.requestCache.has(pollKey)) {
|
|
217
190
|
this.requestCache.set(pollKey, { data: null, timestamp: Date.now() });
|
|
218
|
-
|
|
219
191
|
setTimeout(() => {
|
|
220
192
|
this.requestCache.delete(pollKey);
|
|
221
|
-
// Clear the main cache entry to force fresh request
|
|
222
193
|
this.requestCache.delete(cacheKey);
|
|
223
|
-
// Retry the fetch
|
|
224
194
|
this.fetchAnalytics(filters, onUpdate);
|
|
225
195
|
}, 2000);
|
|
226
196
|
}
|
|
227
|
-
|
|
228
197
|
return;
|
|
229
198
|
}
|
|
230
199
|
|
|
231
|
-
// Process successful response
|
|
232
200
|
const analyticsData = {
|
|
233
201
|
dashboard: data.dashboard,
|
|
234
202
|
leads: data.leads,
|
|
@@ -240,23 +208,23 @@ class AnalyticsService {
|
|
|
240
208
|
isLoading: false,
|
|
241
209
|
};
|
|
242
210
|
|
|
243
|
-
// Cache the response
|
|
244
211
|
this.setCachedResponse(cacheKey, analyticsData);
|
|
245
|
-
|
|
246
|
-
// Update caller
|
|
247
212
|
onUpdate(analyticsData);
|
|
248
213
|
|
|
249
|
-
|
|
214
|
+
// MODIFICATION: Correctly extract top-level availableFilters and update the store
|
|
215
|
+
epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
|
|
216
|
+
...filters,
|
|
217
|
+
availableFilters: data.availableFilters || [],
|
|
218
|
+
});
|
|
250
219
|
|
|
220
|
+
if (VERBOSE) console.log('✅ Analytics request completed successfully');
|
|
251
221
|
this.activeRequest = null;
|
|
252
222
|
} catch (error) {
|
|
253
223
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
254
224
|
if (VERBOSE) console.log('🔄 Request aborted');
|
|
255
225
|
return;
|
|
256
226
|
}
|
|
257
|
-
|
|
258
227
|
console.error('❌ Analytics fetch error:', error);
|
|
259
|
-
|
|
260
228
|
onUpdate({
|
|
261
229
|
dashboard: null,
|
|
262
230
|
leads: null,
|
|
@@ -268,7 +236,6 @@ class AnalyticsService {
|
|
|
268
236
|
error instanceof Error ? error.message : 'Unknown error occurred',
|
|
269
237
|
isLoading: false,
|
|
270
238
|
});
|
|
271
|
-
|
|
272
239
|
this.activeRequest = null;
|
|
273
240
|
}
|
|
274
241
|
}
|
|
@@ -286,13 +253,9 @@ class AnalyticsService {
|
|
|
286
253
|
|
|
287
254
|
initializeFilters(tenantId: string): void {
|
|
288
255
|
if (this.isInitialized) return;
|
|
289
|
-
|
|
290
256
|
if (VERBOSE) console.log('🏁 Initializing analytics filters');
|
|
291
|
-
|
|
292
257
|
const nowUTC = new Date();
|
|
293
258
|
const oneWeekAgoUTC = new Date(nowUTC.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
294
|
-
|
|
295
|
-
// Only set if not already initialized to prevent store churn
|
|
296
259
|
const current = epinetCustomFilters.get();
|
|
297
260
|
if (!current.enabled) {
|
|
298
261
|
epinetCustomFilters.set(tenantId, {
|
|
@@ -301,19 +264,15 @@ class AnalyticsService {
|
|
|
301
264
|
selectedUserId: null,
|
|
302
265
|
startTimeUTC: oneWeekAgoUTC.toISOString(),
|
|
303
266
|
endTimeUTC: nowUTC.toISOString(),
|
|
267
|
+
availableFilters: [],
|
|
268
|
+
appliedFilters: [],
|
|
304
269
|
});
|
|
305
270
|
}
|
|
306
|
-
|
|
307
271
|
this.isInitialized = true;
|
|
308
272
|
}
|
|
309
273
|
|
|
310
274
|
debouncedFetch(filters: any, onUpdate: (data: AnalyticsState) => void): void {
|
|
311
|
-
|
|
312
|
-
if (this.debounceTimer) {
|
|
313
|
-
clearTimeout(this.debounceTimer);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Set new debounced fetch
|
|
275
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
317
276
|
this.debounceTimer = setTimeout(() => {
|
|
318
277
|
this.fetchAnalytics(filters, onUpdate);
|
|
319
278
|
}, this.DEBOUNCE_MS);
|
|
@@ -329,29 +288,21 @@ export default function FetchAnalytics({
|
|
|
329
288
|
|
|
330
289
|
if (VERBOSE) {
|
|
331
290
|
console.log('🔄 FetchAnalytics render', {
|
|
332
|
-
filters: {
|
|
333
|
-
startTimeUTC: $epinetCustomFilters.startTimeUTC,
|
|
334
|
-
endTimeUTC: $epinetCustomFilters.endTimeUTC,
|
|
335
|
-
visitorType: $epinetCustomFilters.visitorType,
|
|
336
|
-
selectedUserId: $epinetCustomFilters.selectedUserId,
|
|
337
|
-
},
|
|
291
|
+
filters: { ...$epinetCustomFilters },
|
|
338
292
|
});
|
|
339
293
|
}
|
|
340
294
|
|
|
341
|
-
// Cleanup on unmount
|
|
342
295
|
useEffect(() => {
|
|
343
296
|
return () => {
|
|
344
297
|
analyticsService.current.cleanup();
|
|
345
298
|
};
|
|
346
299
|
}, []);
|
|
347
300
|
|
|
348
|
-
// Initialize filters once
|
|
349
301
|
useEffect(() => {
|
|
350
302
|
const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
|
|
351
303
|
analyticsService.current.initializeFilters(tenantId);
|
|
352
304
|
}, []);
|
|
353
305
|
|
|
354
|
-
// Debounced fetch when filters change
|
|
355
306
|
useEffect(() => {
|
|
356
307
|
if (
|
|
357
308
|
!$epinetCustomFilters.enabled ||
|
|
@@ -362,15 +313,14 @@ export default function FetchAnalytics({
|
|
|
362
313
|
return;
|
|
363
314
|
}
|
|
364
315
|
|
|
365
|
-
// Create stable filter signature to prevent unnecessary fetches
|
|
366
316
|
const filtersSignature = JSON.stringify({
|
|
367
317
|
startTimeUTC: $epinetCustomFilters.startTimeUTC,
|
|
368
318
|
endTimeUTC: $epinetCustomFilters.endTimeUTC,
|
|
369
319
|
visitorType: $epinetCustomFilters.visitorType,
|
|
370
320
|
selectedUserId: $epinetCustomFilters.selectedUserId,
|
|
321
|
+
appliedFilters: $epinetCustomFilters.appliedFilters,
|
|
371
322
|
});
|
|
372
323
|
|
|
373
|
-
// Skip if filters haven't actually changed
|
|
374
324
|
if (filtersSignature === lastFiltersRef.current) {
|
|
375
325
|
return;
|
|
376
326
|
}
|
|
@@ -389,6 +339,7 @@ export default function FetchAnalytics({
|
|
|
389
339
|
$epinetCustomFilters.selectedUserId,
|
|
390
340
|
$epinetCustomFilters.startTimeUTC,
|
|
391
341
|
$epinetCustomFilters.endTimeUTC,
|
|
342
|
+
$epinetCustomFilters.appliedFilters,
|
|
392
343
|
onAnalyticsUpdate,
|
|
393
344
|
]);
|
|
394
345
|
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { atom } from 'nanostores';
|
|
2
2
|
import { TractStackAPI } from '@/utils/api';
|
|
3
3
|
|
|
4
|
+
interface AvailableFilter {
|
|
5
|
+
beliefSlug: string;
|
|
6
|
+
values: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AppliedFilter {
|
|
10
|
+
beliefSlug: string;
|
|
11
|
+
value: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
// Internal tenant-keyed storage
|
|
5
15
|
const tenantEpinetCustomFilters = atom<
|
|
6
16
|
Record<
|
|
@@ -22,6 +32,8 @@ const tenantEpinetCustomFilters = atom<
|
|
|
22
32
|
}
|
|
23
33
|
>
|
|
24
34
|
>;
|
|
35
|
+
availableFilters: AvailableFilter[];
|
|
36
|
+
appliedFilters: AppliedFilter[];
|
|
25
37
|
}
|
|
26
38
|
>
|
|
27
39
|
>({});
|
|
@@ -53,6 +65,8 @@ const defaultEpinetFilters = {
|
|
|
53
65
|
endTimeUTC: null,
|
|
54
66
|
userCounts: [],
|
|
55
67
|
hourlyNodeActivity: {},
|
|
68
|
+
availableFilters: [],
|
|
69
|
+
appliedFilters: [],
|
|
56
70
|
};
|
|
57
71
|
|
|
58
72
|
// Create tenant-aware atoms that work with useStore
|