astro-tractstack 2.2.4 → 2.2.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/package.json
CHANGED
|
@@ -8,6 +8,7 @@ import DashboardActivity from './Dashboard_Activity';
|
|
|
8
8
|
import SankeyDiagram from '../codehooks/SankeyDiagram';
|
|
9
9
|
import EpinetDurationSelector from '../codehooks/EpinetDurationSelector';
|
|
10
10
|
import FetchAnalytics from './state/FetchAnalytics';
|
|
11
|
+
import type { PulseMetrics, SystemMetrics } from './state/FetchAnalytics';
|
|
11
12
|
import type { FullContentMapItem } from '@/types/tractstack';
|
|
12
13
|
|
|
13
14
|
interface StoryKeepDashboardAnalyticsProps {
|
|
@@ -15,12 +16,16 @@ interface StoryKeepDashboardAnalyticsProps {
|
|
|
15
16
|
initializing?: boolean;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
interface ErrorBoundaryProps {
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
fallback: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
class ErrorBoundary extends Component<
|
|
20
|
-
|
|
25
|
+
ErrorBoundaryProps,
|
|
21
26
|
{ hasError: boolean }
|
|
22
27
|
> {
|
|
23
|
-
constructor(props:
|
|
28
|
+
constructor(props: ErrorBoundaryProps) {
|
|
24
29
|
super(props);
|
|
25
30
|
this.state = { hasError: false };
|
|
26
31
|
}
|
|
@@ -37,6 +42,70 @@ class ErrorBoundary extends Component<
|
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
// Types derived from Dashboard_Activity.tsx
|
|
46
|
+
interface ActivitySeries {
|
|
47
|
+
id: string;
|
|
48
|
+
data: Array<{ x: any; y: number }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface DashboardData {
|
|
52
|
+
stats: {
|
|
53
|
+
daily: number;
|
|
54
|
+
weekly: number;
|
|
55
|
+
monthly: number;
|
|
56
|
+
};
|
|
57
|
+
line: ActivitySeries[];
|
|
58
|
+
dailyAnonymous: number;
|
|
59
|
+
dailyKnown: number;
|
|
60
|
+
weeklyAnonymous: number;
|
|
61
|
+
weeklyKnown: number;
|
|
62
|
+
monthlyAnonymous: number;
|
|
63
|
+
monthlyKnown: number;
|
|
64
|
+
status?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface LeadsData {
|
|
68
|
+
totalLeads: number;
|
|
69
|
+
status?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Types derived from SankeyDiagram.tsx
|
|
73
|
+
interface SankeyNode {
|
|
74
|
+
name: string;
|
|
75
|
+
id: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface SankeyLink {
|
|
79
|
+
source: number;
|
|
80
|
+
target: number;
|
|
81
|
+
value: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface EpinetData {
|
|
85
|
+
nodes: SankeyNode[];
|
|
86
|
+
links: SankeyLink[];
|
|
87
|
+
status?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Types derived from EpinetDurationSelector.tsx usage
|
|
91
|
+
interface UserCountItem {
|
|
92
|
+
id: string;
|
|
93
|
+
count: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface AnalyticsState {
|
|
97
|
+
dashboard: DashboardData | null;
|
|
98
|
+
leads: LeadsData | null;
|
|
99
|
+
epinet: EpinetData | null;
|
|
100
|
+
userCounts: UserCountItem[];
|
|
101
|
+
hourlyNodeActivity: Record<string, number>;
|
|
102
|
+
pulse: PulseMetrics | null;
|
|
103
|
+
system: SystemMetrics | null;
|
|
104
|
+
isLoading: boolean;
|
|
105
|
+
status: string;
|
|
106
|
+
error: string | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
40
109
|
const DurationSelector = ({
|
|
41
110
|
currentDurationHelper,
|
|
42
111
|
setStandardDuration,
|
|
@@ -83,22 +152,14 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
83
152
|
const $epinetCustomFilters = useStore(epinetCustomFilters);
|
|
84
153
|
const [isDownloading, setIsDownloading] = useState(false);
|
|
85
154
|
|
|
86
|
-
|
|
87
|
-
const [analytics, setAnalytics] = useState<{
|
|
88
|
-
dashboard: any;
|
|
89
|
-
leads: any;
|
|
90
|
-
epinet: any;
|
|
91
|
-
userCounts: any[];
|
|
92
|
-
hourlyNodeActivity: any;
|
|
93
|
-
isLoading: boolean;
|
|
94
|
-
status: string;
|
|
95
|
-
error: string | null;
|
|
96
|
-
}>({
|
|
155
|
+
const [analytics, setAnalytics] = useState<AnalyticsState>({
|
|
97
156
|
dashboard: null,
|
|
98
157
|
leads: null,
|
|
99
158
|
epinet: null,
|
|
100
159
|
userCounts: [],
|
|
101
160
|
hourlyNodeActivity: {},
|
|
161
|
+
pulse: null,
|
|
162
|
+
system: null,
|
|
102
163
|
isLoading: false,
|
|
103
164
|
status: 'idle',
|
|
104
165
|
error: null,
|
|
@@ -128,7 +189,6 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
128
189
|
});
|
|
129
190
|
};
|
|
130
191
|
|
|
131
|
-
// Duration helper for UI
|
|
132
192
|
const currentDurationHelper = useMemo(():
|
|
133
193
|
| 'daily'
|
|
134
194
|
| 'weekly'
|
|
@@ -151,7 +211,6 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
151
211
|
return 'monthly';
|
|
152
212
|
}, [$epinetCustomFilters.startTimeUTC, $epinetCustomFilters.endTimeUTC]);
|
|
153
213
|
|
|
154
|
-
// Standard duration setter helper
|
|
155
214
|
const setStandardDuration = useCallback(
|
|
156
215
|
(newValue: 'daily' | 'weekly' | 'monthly') => {
|
|
157
216
|
const nowUTC = new Date();
|
|
@@ -171,7 +230,6 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
171
230
|
[$epinetCustomFilters]
|
|
172
231
|
);
|
|
173
232
|
|
|
174
|
-
// Download leads CSV
|
|
175
233
|
const downloadLeadsCSV = async () => {
|
|
176
234
|
if (isDownloading) return;
|
|
177
235
|
try {
|
|
@@ -212,12 +270,10 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
212
270
|
}
|
|
213
271
|
};
|
|
214
272
|
|
|
215
|
-
// Helper function for number formatting
|
|
216
273
|
const formatNumber = (num: number) => {
|
|
217
274
|
return new Intl.NumberFormat().format(num);
|
|
218
275
|
};
|
|
219
276
|
|
|
220
|
-
// Prepare stats data for display
|
|
221
277
|
const stats = [
|
|
222
278
|
{
|
|
223
279
|
name: 'Past 24 Hours',
|
|
@@ -263,6 +319,72 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
263
319
|
</div>
|
|
264
320
|
)}
|
|
265
321
|
|
|
322
|
+
{/* Live Operations Row */}
|
|
323
|
+
<div className="mb-6 grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
324
|
+
{/* Card 1: Active Visitors */}
|
|
325
|
+
<div className="rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm transition-colors hover:border-cyan-100">
|
|
326
|
+
<div className="flex items-center gap-2">
|
|
327
|
+
<span className="relative flex h-2.5 w-2.5">
|
|
328
|
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
|
329
|
+
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-green-500"></span>
|
|
330
|
+
</span>
|
|
331
|
+
<dt className="text-sm font-bold text-gray-800">Active Visitors</dt>
|
|
332
|
+
</div>
|
|
333
|
+
<dd className="mt-2">
|
|
334
|
+
<div className="text-2xl font-bold tracking-tight text-cyan-700">
|
|
335
|
+
{analytics.pulse
|
|
336
|
+
? formatNumber(analytics.pulse.activeVisitors)
|
|
337
|
+
: '-'}
|
|
338
|
+
</div>
|
|
339
|
+
</dd>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
{/* Card 2: Traffic Velocity */}
|
|
343
|
+
<div className="rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm transition-colors hover:border-cyan-100">
|
|
344
|
+
<dt className="text-sm font-bold text-gray-800">Traffic Velocity</dt>
|
|
345
|
+
<dd className="mt-2">
|
|
346
|
+
<div className="text-2xl font-bold tracking-tight text-cyan-700">
|
|
347
|
+
{analytics.pulse ? formatNumber(analytics.pulse.velocity) : '-'}{' '}
|
|
348
|
+
<span className="text-sm font-normal text-gray-500">/ Hr</span>
|
|
349
|
+
</div>
|
|
350
|
+
<div className="mt-1 text-xs text-gray-600">
|
|
351
|
+
Events (Last Full Hour)
|
|
352
|
+
</div>
|
|
353
|
+
</dd>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
{/* Card 3: System Health */}
|
|
357
|
+
<div className="rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm transition-colors hover:border-cyan-100">
|
|
358
|
+
<dt className="text-sm font-bold text-gray-800">System Health</dt>
|
|
359
|
+
<dd className="mt-2">
|
|
360
|
+
{analytics.system ? (
|
|
361
|
+
<>
|
|
362
|
+
<div className="flex items-baseline gap-2">
|
|
363
|
+
<span
|
|
364
|
+
className={`text-2xl font-bold tracking-tight ${analytics.system.waitCount > 0 ? 'text-yellow-600' : 'text-green-600'}`}
|
|
365
|
+
>
|
|
366
|
+
{analytics.system.waitCount > 0 ? 'Busy' : 'Healthy'}
|
|
367
|
+
</span>
|
|
368
|
+
</div>
|
|
369
|
+
<div className="mt-1 text-xs text-gray-600">
|
|
370
|
+
DB Load:{' '}
|
|
371
|
+
{analytics.system.maxOpenConns > 0
|
|
372
|
+
? Math.round(
|
|
373
|
+
(analytics.system.openConns /
|
|
374
|
+
analytics.system.maxOpenConns) *
|
|
375
|
+
100
|
|
376
|
+
)
|
|
377
|
+
: 0}
|
|
378
|
+
%
|
|
379
|
+
</div>
|
|
380
|
+
</>
|
|
381
|
+
) : (
|
|
382
|
+
<div className="text-2xl font-bold text-gray-300">-</div>
|
|
383
|
+
)}
|
|
384
|
+
</dd>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
266
388
|
{/* Stats Cards Grid */}
|
|
267
389
|
<div className="mb-6 grid grid-cols-3 gap-4">
|
|
268
390
|
{stats.map((item) => {
|
|
@@ -305,7 +427,6 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
305
427
|
<hr className="my-1.5 border-gray-100 md:my-3.5" />
|
|
306
428
|
|
|
307
429
|
<dd>
|
|
308
|
-
{/* Desktop: side-by-side layout */}
|
|
309
430
|
<div className="hidden items-end justify-between md:flex">
|
|
310
431
|
<div className="flex-1">
|
|
311
432
|
<div className="text-sm text-gray-600">
|
|
@@ -327,7 +448,6 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
327
448
|
</div>
|
|
328
449
|
</div>
|
|
329
450
|
|
|
330
|
-
{/* Mobile: stacked layout */}
|
|
331
451
|
<div className="md:hidden">
|
|
332
452
|
<div className="mb-1.5">
|
|
333
453
|
<div className="text-sm text-gray-600">
|
|
@@ -353,7 +473,6 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
353
473
|
);
|
|
354
474
|
})}
|
|
355
475
|
|
|
356
|
-
{/* Total Leads Card */}
|
|
357
476
|
<div className="col-span-3 rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm transition-colors hover:border-cyan-100">
|
|
358
477
|
<div className="flex items-center justify-between">
|
|
359
478
|
<dt className="text-sm font-bold text-gray-800">Total Leads</dt>
|
|
@@ -381,13 +500,11 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
381
500
|
</div>
|
|
382
501
|
</div>
|
|
383
502
|
|
|
384
|
-
{/* Duration Selector */}
|
|
385
503
|
<DurationSelector
|
|
386
504
|
currentDurationHelper={currentDurationHelper}
|
|
387
505
|
setStandardDuration={setStandardDuration}
|
|
388
506
|
/>
|
|
389
507
|
|
|
390
|
-
{/* Dashboard Activity Chart */}
|
|
391
508
|
<div className="mb-6 overflow-hidden">
|
|
392
509
|
<h3 className="mb-4 text-lg font-bold text-gray-900">
|
|
393
510
|
Activity Over Time
|
|
@@ -412,12 +529,10 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
412
529
|
)}
|
|
413
530
|
</div>
|
|
414
531
|
|
|
415
|
-
{/* Separator */}
|
|
416
532
|
<div className="my-8">
|
|
417
533
|
<hr className="border-gray-200" />
|
|
418
534
|
</div>
|
|
419
535
|
|
|
420
|
-
{/* User Journey Section */}
|
|
421
536
|
<div className="mb-6 overflow-visible">
|
|
422
537
|
<h3 className="mb-4 text-lg font-bold text-gray-900">
|
|
423
538
|
User Journey Analytics
|
|
@@ -466,7 +581,6 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
466
581
|
analytics.isLoading || analytics.status === 'loading'
|
|
467
582
|
}
|
|
468
583
|
hourlyNodeActivity={analytics.hourlyNodeActivity}
|
|
469
|
-
// MODIFICATION: Read availableFilters from the store, not local state
|
|
470
584
|
availableFilters={$epinetCustomFilters.availableFilters}
|
|
471
585
|
appliedFilters={$epinetCustomFilters.appliedFilters}
|
|
472
586
|
onBeliefFilterChange={handleBeliefFilterChange}
|
|
@@ -485,7 +599,6 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
485
599
|
analytics.isLoading || analytics.status === 'loading'
|
|
486
600
|
}
|
|
487
601
|
hourlyNodeActivity={analytics.hourlyNodeActivity}
|
|
488
|
-
// MODIFICATION: Read availableFilters from the store, not local state
|
|
489
602
|
availableFilters={$epinetCustomFilters.availableFilters}
|
|
490
603
|
appliedFilters={$epinetCustomFilters.appliedFilters}
|
|
491
604
|
onBeliefFilterChange={handleBeliefFilterChange}
|
|
@@ -502,7 +615,6 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
502
615
|
fullContentMap={fullContentMap}
|
|
503
616
|
isLoading={analytics.isLoading || analytics.status === 'loading'}
|
|
504
617
|
hourlyNodeActivity={analytics.hourlyNodeActivity}
|
|
505
|
-
// MODIFICATION: Read availableFilters from the store, not local state
|
|
506
618
|
availableFilters={$epinetCustomFilters.availableFilters}
|
|
507
619
|
appliedFilters={$epinetCustomFilters.appliedFilters}
|
|
508
620
|
onBeliefFilterChange={handleBeliefFilterChange}
|
|
@@ -5,12 +5,29 @@ import { TractStackAPI } from '@/utils/api';
|
|
|
5
5
|
|
|
6
6
|
const VERBOSE = false;
|
|
7
7
|
|
|
8
|
+
export interface PulseMetrics {
|
|
9
|
+
activeVisitors: number;
|
|
10
|
+
velocity: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SystemMetrics {
|
|
14
|
+
available: boolean;
|
|
15
|
+
openConns: number;
|
|
16
|
+
inUse: number;
|
|
17
|
+
idle: number;
|
|
18
|
+
waitCount: number;
|
|
19
|
+
waitDuration: string;
|
|
20
|
+
maxOpenConns: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
interface AnalyticsState {
|
|
9
24
|
dashboard: any;
|
|
10
25
|
leads: any;
|
|
11
26
|
epinet: any;
|
|
12
27
|
userCounts: any[];
|
|
13
28
|
hourlyNodeActivity: any;
|
|
29
|
+
pulse: PulseMetrics | null;
|
|
30
|
+
system: SystemMetrics | null;
|
|
14
31
|
isLoading: boolean;
|
|
15
32
|
status: string;
|
|
16
33
|
error: string | null;
|
|
@@ -127,7 +144,6 @@ class AnalyticsService {
|
|
|
127
144
|
if (filters.selectedUserId)
|
|
128
145
|
params.append('userId', filters.selectedUserId);
|
|
129
146
|
|
|
130
|
-
// MODIFICATION: Properly format appliedFilters for the backend
|
|
131
147
|
if (filters.appliedFilters && filters.appliedFilters.length > 0) {
|
|
132
148
|
params.append('appliedFilters', JSON.stringify(filters.appliedFilters));
|
|
133
149
|
}
|
|
@@ -145,6 +161,8 @@ class AnalyticsService {
|
|
|
145
161
|
epinet: null,
|
|
146
162
|
userCounts: [],
|
|
147
163
|
hourlyNodeActivity: {},
|
|
164
|
+
pulse: null,
|
|
165
|
+
system: null,
|
|
148
166
|
isLoading: true,
|
|
149
167
|
status: 'loading',
|
|
150
168
|
error: null,
|
|
@@ -153,13 +171,31 @@ class AnalyticsService {
|
|
|
153
171
|
const api = new TractStackAPI(
|
|
154
172
|
window.TRACTSTACK_CONFIG?.tenantId || 'default'
|
|
155
173
|
);
|
|
174
|
+
|
|
156
175
|
const endpoint = `/api/v1/analytics/all${params.toString() ? `?${params.toString()}` : ''}`;
|
|
157
|
-
if (VERBOSE) console.log('🔥 Making API request', { endpoint });
|
|
158
176
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
177
|
+
if (VERBOSE) {
|
|
178
|
+
console.log('🔥 Making API requests', {
|
|
179
|
+
analytics: endpoint,
|
|
180
|
+
pulse: '/api/v1/admin/pulse',
|
|
181
|
+
system: '/api/v1/admin/db/stats',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const [analyticsResponse, pulseResponse, systemResponse] =
|
|
186
|
+
await Promise.all([
|
|
187
|
+
api.get(endpoint),
|
|
188
|
+
api.get('/api/v1/admin/pulse'),
|
|
189
|
+
api.get('/api/v1/admin/db/stats'),
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
if (!analyticsResponse.success) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
analyticsResponse.error || 'Failed to fetch analytics data'
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const data = analyticsResponse.data;
|
|
163
199
|
|
|
164
200
|
const isStillLoading =
|
|
165
201
|
data?.status === 'loading' ||
|
|
@@ -173,12 +209,15 @@ class AnalyticsService {
|
|
|
173
209
|
|
|
174
210
|
if (isStillLoading) {
|
|
175
211
|
if (VERBOSE) console.log('⏳ Backend data still loading, will poll...');
|
|
212
|
+
|
|
176
213
|
const partialAnalytics = {
|
|
177
214
|
dashboard: data.dashboard,
|
|
178
215
|
leads: data.leads,
|
|
179
216
|
epinet: data.epinet,
|
|
180
217
|
userCounts: data.userCounts || [],
|
|
181
218
|
hourlyNodeActivity: data.hourlyNodeActivity || {},
|
|
219
|
+
pulse: pulseResponse.success ? pulseResponse.data?.pulse : null,
|
|
220
|
+
system: systemResponse.success ? systemResponse.data : null,
|
|
182
221
|
status: 'loading',
|
|
183
222
|
error: null,
|
|
184
223
|
isLoading: true,
|
|
@@ -203,6 +242,8 @@ class AnalyticsService {
|
|
|
203
242
|
epinet: data.epinet,
|
|
204
243
|
userCounts: data.userCounts || [],
|
|
205
244
|
hourlyNodeActivity: data.hourlyNodeActivity || {},
|
|
245
|
+
pulse: pulseResponse.success ? pulseResponse.data?.pulse : null,
|
|
246
|
+
system: systemResponse.success ? systemResponse.data : null,
|
|
206
247
|
status: 'complete',
|
|
207
248
|
error: null,
|
|
208
249
|
isLoading: false,
|
|
@@ -211,13 +252,12 @@ class AnalyticsService {
|
|
|
211
252
|
this.setCachedResponse(cacheKey, analyticsData);
|
|
212
253
|
onUpdate(analyticsData);
|
|
213
254
|
|
|
214
|
-
// MODIFICATION: Correctly extract top-level availableFilters and update the store
|
|
215
255
|
epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
|
|
216
256
|
...filters,
|
|
217
257
|
availableFilters: data.availableFilters || [],
|
|
218
258
|
});
|
|
219
259
|
|
|
220
|
-
if (VERBOSE) console.log('✅ Analytics
|
|
260
|
+
if (VERBOSE) console.log('✅ Analytics requests completed successfully');
|
|
221
261
|
this.activeRequest = null;
|
|
222
262
|
} catch (error) {
|
|
223
263
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
@@ -231,6 +271,8 @@ class AnalyticsService {
|
|
|
231
271
|
epinet: null,
|
|
232
272
|
userCounts: [],
|
|
233
273
|
hourlyNodeActivity: {},
|
|
274
|
+
pulse: null,
|
|
275
|
+
system: null,
|
|
234
276
|
status: 'error',
|
|
235
277
|
error:
|
|
236
278
|
error instanceof Error ? error.message : 'Unknown error occurred',
|