Skip to content

Commit df7c87a

Browse files
hemanandrclaude
andcommitted
feat: implement comprehensive Mixpanel analytics integration
This commit implements a privacy-first analytics solution using Mixpanel with explicit consent management and manufacturing-specific KPI tracking. Key Features: - Privacy-by-design with granular consent controls - Manufacturing KPI tracking (MTTD, MTTR, availability metrics) - Page view and interaction tracking across all major components - Comprehensive analytics service with PII sanitization - Integration with existing telemetry consent system Components Added: - AnalyticsService: Core privacy-first Mixpanel wrapper - useAnalyticsConsentInit: Consent-aware initialization hook - useAnalytics: Enhanced analytics tracking with manufacturing methods - manufacturingAnalytics: Utility functions for KPI calculations Page Analytics Integration: - Dashboard: System metrics and KPI tracking - History: Performance analysis tracking - Settings: Privacy controls interaction tracking - Configuration: YAML config change tracking - StatusTable: Endpoint interaction tracking Privacy Compliance: - Only initializes when explicit consent granted - No PII collection (sendDefaultPii: false) - Granular consent for usage vs error diagnostics - Data sanitization for all tracked properties - Opt-out by default with explicit opt-in required 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 7e73595 commit df7c87a

File tree

13 files changed

+1153
-10
lines changed

13 files changed

+1153
-10
lines changed

thingconnect.pulse.client/package-lock.json

Lines changed: 291 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

thingconnect.pulse.client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"lodash": "^4.17.21",
2424
"lucide-react": "^0.541.0",
2525
"luxon": "^3.7.1",
26+
"mixpanel-browser": "^2.70.0",
2627
"monaco-editor": "^0.52.2",
2728
"next-themes": "^0.4.6",
2829
"react": "^19.1.1",

thingconnect.pulse.client/src/components/status/StatusTable.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Box, Text, Badge, HStack, Skeleton } from '@chakra-ui/react';
22
import { Table } from '@chakra-ui/react';
33
import { formatDistanceToNow } from 'date-fns';
44
import { useNavigate } from 'react-router-dom';
5+
import { useAnalytics } from '@/hooks/useAnalytics';
56
import type { LiveStatusItem } from '@/api/types';
67
import TrendBlocks from './TrendBlocks';
78

@@ -12,6 +13,7 @@ interface StatusTableProps {
1213

1314
export function StatusTable({ items, isLoading }: StatusTableProps) {
1415
const navigate = useNavigate();
16+
const analytics = useAnalytics();
1517

1618
const getStatusColor = (status: string) => {
1719
switch (status.toLowerCase()) {
@@ -40,6 +42,13 @@ export function StatusTable({ items, isLoading }: StatusTableProps) {
4042
};
4143

4244
const handleRowClick = (id: string) => {
45+
// Track endpoint selection
46+
analytics.trackDashboardInteraction('endpoint_details_click', {
47+
table_type: 'status_overview',
48+
endpoint_id: id,
49+
source: 'main_dashboard'
50+
});
51+
4352
void navigate(`/endpoints/${id}`);
4453
};
4554
console.log('isLoading in StatusTable:', items);

thingconnect.pulse.client/src/features/auth/context/AuthContext.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type RegisterRequest,
88
} from '../services/authService';
99
import { useSentryConsentInit } from '../../../hooks/useSentryConsentInit';
10+
import { useAnalyticsConsentInit } from '../../../hooks/useAnalyticsConsentInit';
1011

1112
interface AuthContextType {
1213
user: UserInfo | null;
@@ -35,6 +36,9 @@ export function AuthProvider({ children }: AuthProviderProps) {
3536
// Initialize Sentry based on user consent when authenticated
3637
useSentryConsentInit(isAuthenticated);
3738

39+
// Initialize Analytics based on user consent when authenticated
40+
const { analytics } = useAnalyticsConsentInit(isAuthenticated);
41+
3842
const checkSession = async () => {
3943
try {
4044
setIsLoading(true);
@@ -69,6 +73,14 @@ export function AuthProvider({ children }: AuthProviderProps) {
6973
const userData = await authService.login(credentials);
7074
setUser(userData);
7175
setSetupRequired(false);
76+
77+
// Track successful login (after analytics is initialized)
78+
setTimeout(() => {
79+
analytics.track('User Login', {
80+
user_role: userData.role,
81+
login_method: 'password'
82+
});
83+
}, 1000);
7284
} catch (error) {
7385
console.error('Login failed:', error);
7486
throw error;
@@ -80,6 +92,14 @@ export function AuthProvider({ children }: AuthProviderProps) {
8092
const newUser = await authService.register(userData);
8193
setUser(newUser);
8294
setSetupRequired(false);
95+
96+
// Track user registration (after analytics is initialized)
97+
setTimeout(() => {
98+
analytics.track('User Registered', {
99+
user_role: newUser.role,
100+
registration_method: 'onboarding_flow'
101+
});
102+
}, 1000);
83103
} catch (error) {
84104
console.error('Registration failed:', error);
85105
throw error;
@@ -88,11 +108,18 @@ export function AuthProvider({ children }: AuthProviderProps) {
88108

89109
const logout = async () => {
90110
try {
111+
// Track logout before clearing session
112+
analytics.track('User Logout');
113+
91114
await authService.logout();
92115
} catch (error) {
93116
console.error('Logout error:', error);
94117
} finally {
95118
setUser(null);
119+
120+
// Reset analytics session
121+
analytics.reset();
122+
96123
// After logout, check if setup is needed
97124
try {
98125
const needsSetup = await authService.isSetupRequired();
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { getAnalyticsService } from './useAnalyticsConsentInit';
2+
3+
export function useAnalytics() {
4+
const analytics = getAnalyticsService();
5+
6+
const trackPageView = (pageName: string, properties?: Record<string, unknown>) => {
7+
analytics.track('Page View', {
8+
page_name: pageName,
9+
timestamp: new Date().toISOString(),
10+
session_duration: performance.now(),
11+
...properties
12+
});
13+
};
14+
15+
const trackFeatureUsage = (featureName: string, action: string, properties?: Record<string, unknown>) => {
16+
analytics.track('Feature Used', {
17+
feature_name: featureName,
18+
action,
19+
timestamp: new Date().toISOString(),
20+
...properties
21+
});
22+
};
23+
24+
const trackMonitoringAction = (action: string, properties?: Record<string, unknown>) => {
25+
analytics.track('Monitoring Action', {
26+
action,
27+
timestamp: new Date().toISOString(),
28+
...properties
29+
});
30+
};
31+
32+
const trackConfigurationChange = (changeType: string, properties?: Record<string, unknown>) => {
33+
analytics.track('Configuration Changed', {
34+
change_type: changeType,
35+
timestamp: new Date().toISOString(),
36+
...properties
37+
});
38+
};
39+
40+
const trackDashboardInteraction = (interactionType: string, properties?: Record<string, unknown>) => {
41+
analytics.track('Dashboard Interaction', {
42+
interaction_type: interactionType,
43+
timestamp: new Date().toISOString(),
44+
...properties
45+
});
46+
};
47+
48+
// Manufacturing-specific tracking methods
49+
const trackEndpointManagement = (action: string, endpointData?: Record<string, unknown>) => {
50+
analytics.track('Endpoint Management', {
51+
action, // 'create', 'update', 'delete', 'test'
52+
endpoint_type: endpointData?.type,
53+
probe_interval: endpointData?.interval,
54+
timeout_seconds: endpointData?.timeout,
55+
has_authentication: !!endpointData?.authentication,
56+
timestamp: new Date().toISOString(),
57+
...endpointData
58+
});
59+
};
60+
61+
const trackAlertInteraction = (action: string, alertData?: Record<string, unknown>) => {
62+
analytics.track('Alert Interaction', {
63+
action, // 'acknowledge', 'resolve', 'escalate', 'view_details'
64+
alert_severity: alertData?.severity,
65+
alert_type: alertData?.type,
66+
response_time_seconds: alertData?.responseTime,
67+
resolution_method: alertData?.resolutionMethod,
68+
timestamp: new Date().toISOString()
69+
});
70+
};
71+
72+
const trackSystemMetrics = (metrics: Record<string, unknown>) => {
73+
analytics.track('System Metrics', {
74+
total_endpoints: metrics.totalEndpoints,
75+
active_alerts: metrics.activeAlerts,
76+
overall_availability: metrics.overallAvailability,
77+
monitored_services: metrics.monitoredServices,
78+
data_retention_days: metrics.dataRetentionDays,
79+
uptime_percentage: metrics.uptimePercentage,
80+
timestamp: new Date().toISOString()
81+
});
82+
};
83+
84+
const trackPerformanceMetrics = (pageName: string, metrics?: Record<string, unknown>) => {
85+
analytics.track('Performance Metrics', {
86+
page_name: pageName,
87+
load_time_ms: metrics?.loadTime,
88+
data_fetch_time_ms: metrics?.dataFetchTime,
89+
render_time_ms: metrics?.renderTime,
90+
memory_usage_mb: 'memory' in performance ? Math.round((performance as any).memory.usedJSHeapSize / 1024 / 1024) : undefined,
91+
timestamp: new Date().toISOString()
92+
});
93+
};
94+
95+
const trackManufacturingKPIs = (kpiData: Record<string, unknown>) => {
96+
analytics.track('Manufacturing KPIs', {
97+
monitoring_coverage_percentage: kpiData.monitoringCoverage,
98+
mean_time_to_detection_seconds: kpiData.mttd,
99+
mean_time_to_recovery_seconds: kpiData.mttr,
100+
false_positive_rate: kpiData.falsePositiveRate,
101+
network_segments_monitored: kpiData.networkSegments,
102+
critical_endpoints_count: kpiData.criticalEndpoints,
103+
timestamp: new Date().toISOString()
104+
});
105+
};
106+
107+
const trackConfigurationComplexity = (configData: Record<string, unknown>) => {
108+
analytics.track('Configuration Complexity', {
109+
total_rules: configData.totalRules,
110+
unique_probe_types: configData.uniqueProbeTypes,
111+
custom_intervals_count: configData.customIntervals,
112+
advanced_features_used: configData.advancedFeatures,
113+
configuration_size_kb: configData.configSizeKb,
114+
validation_errors: configData.validationErrors,
115+
timestamp: new Date().toISOString()
116+
});
117+
};
118+
119+
const trackUserEfficiency = (efficiencyData: Record<string, unknown>) => {
120+
analytics.track('User Efficiency', {
121+
tasks_completed_per_session: efficiencyData.tasksCompleted,
122+
average_task_duration_seconds: efficiencyData.avgTaskDuration,
123+
navigation_depth: efficiencyData.navigationDepth,
124+
help_usage_count: efficiencyData.helpUsage,
125+
keyboard_shortcuts_used: efficiencyData.keyboardShortcuts,
126+
timestamp: new Date().toISOString()
127+
});
128+
};
129+
130+
return {
131+
track: analytics.track.bind(analytics),
132+
trackPageView,
133+
trackFeatureUsage,
134+
trackMonitoringAction,
135+
trackConfigurationChange,
136+
trackDashboardInteraction,
137+
// Manufacturing-specific methods
138+
trackEndpointManagement,
139+
trackAlertInteraction,
140+
trackSystemMetrics,
141+
trackPerformanceMetrics,
142+
trackManufacturingKPIs,
143+
trackConfigurationComplexity,
144+
trackUserEfficiency,
145+
isInitialized: analytics.isInitialized()
146+
};
147+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { useEffect, useState } from 'react';
2+
import { analytics, privacyFirstAnalytics } from '../services/analytics.service';
3+
4+
let analyticsInitialized = false;
5+
let currentAnalyticsService = analytics; // Start with no-op
6+
7+
export function useAnalyticsConsentInit(isAuthenticated: boolean) {
8+
const [initializationAttempted, setInitializationAttempted] = useState(false);
9+
10+
useEffect(() => {
11+
const initializeAnalyticsIfConsented = async () => {
12+
// Only attempt initialization once and when user is authenticated
13+
if (!isAuthenticated || initializationAttempted || analyticsInitialized) {
14+
return;
15+
}
16+
17+
setInitializationAttempted(true);
18+
19+
try {
20+
// Check if user has consented to usage analytics
21+
const consentResponse = await fetch('/api/auth/telemetry-consent', {
22+
credentials: 'include' // Include cookies for authentication
23+
});
24+
25+
if (consentResponse.ok) {
26+
const consent = await consentResponse.json() as { errorDiagnostics: boolean; usageAnalytics: boolean };
27+
28+
if (consent.usageAnalytics) {
29+
// Initialize privacy-first analytics service
30+
privacyFirstAnalytics.init();
31+
currentAnalyticsService = privacyFirstAnalytics;
32+
analyticsInitialized = true;
33+
34+
// Track initial session start
35+
currentAnalyticsService.track('Session Started', {
36+
authentication_method: 'web_login',
37+
platform: 'web',
38+
is_mobile: window.innerWidth < 768
39+
});
40+
41+
console.log('Analytics initialized with user consent for usage analytics');
42+
} else {
43+
console.log('Analytics not initialized - user has not consented to usage analytics');
44+
}
45+
} else {
46+
console.log('Analytics not initialized - could not verify consent');
47+
}
48+
} catch (error) {
49+
console.log('Analytics initialization skipped - consent verification failed:', error);
50+
}
51+
};
52+
53+
void initializeAnalyticsIfConsented();
54+
}, [isAuthenticated, initializationAttempted]);
55+
56+
return {
57+
analyticsInitialized,
58+
analytics: currentAnalyticsService
59+
};
60+
}
61+
62+
// Export the current analytics service for use throughout the app
63+
export const getAnalyticsService = () => currentAnalyticsService;

thingconnect.pulse.client/src/pages/Configuration.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
1-
import { useState } from 'react';
1+
import { useState, useEffect } from 'react';
22
import { Text } from '@chakra-ui/react';
33
import { TabsRoot, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
44
import { ConfigurationEditor } from '@/components/config/ConfigurationEditor';
55
import { ConfigurationVersions } from '@/components/config/ConfigurationVersions';
66
import { Page } from '@/components/layout/Page';
7+
import { useAnalytics } from '@/hooks/useAnalytics';
78

89
export default function Configuration() {
10+
const analytics = useAnalytics();
911
const [refreshTrigger, setRefreshTrigger] = useState(0);
12+
13+
useEffect(() => {
14+
analytics.trackPageView('Configuration', {
15+
view_type: 'yaml_configuration',
16+
section: 'monitoring_setup'
17+
});
18+
}, []);
19+
1020
const handleConfigurationApplied = () => {
1121
setRefreshTrigger(prev => prev + 1);
22+
23+
// Track configuration change
24+
analytics.trackConfigurationChange('yaml_config_applied', {
25+
source: 'manual_edit',
26+
validation_passed: true
27+
});
1228
};
1329

1430
return (

0 commit comments

Comments
 (0)