Implement Phase 7 Performance Optimization and fix tracking system
Phase 7 Implementation: - Add lazy loading for AI Assistant and Insights pages - Create LoadingFallback component with skeleton screens (page, card, list, chart, chat variants) - Create OptimizedImage component with Next.js Image optimization - Create PerformanceMonitor component with web-vitals v5 integration - Add performance monitoring library tracking Core Web Vitals (CLS, INP, FCP, LCP, TTFB) - Install web-vitals v5.1.0 dependency - Extract AI chat interface and insights dashboard to lazy-loaded components Tracking System Fixes: - Fix API data transformation between frontend (timestamp/data) and backend (startedAt/metadata) - Update createActivity, getActivities, and getActivity to properly transform data structures - Fix diaper, feeding, and sleep tracking pages to work with backend API Homepage Improvements: - Connect Today's Summary to backend daily summary API - Load real-time data for feeding count, sleep hours, and diaper count - Add loading states and empty states for better UX - Format sleep duration as "Xh Ym" for better readability - Display child name in summary section 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -44,19 +44,44 @@ export const trackingApi = {
|
||||
if (endDate) params.endDate = endDate;
|
||||
|
||||
const response = await apiClient.get('/api/v1/activities', { params });
|
||||
return response.data.data.activities;
|
||||
// Transform backend response to frontend format
|
||||
const activities = response.data.data.activities.map((activity: any) => ({
|
||||
...activity,
|
||||
timestamp: activity.startedAt, // Frontend expects timestamp
|
||||
data: activity.metadata, // Frontend expects data
|
||||
}));
|
||||
return activities;
|
||||
},
|
||||
|
||||
// Get a specific activity
|
||||
getActivity: async (id: string): Promise<Activity> => {
|
||||
const response = await apiClient.get(`/api/v1/activities/${id}`);
|
||||
return response.data.data.activity;
|
||||
const activity = response.data.data.activity;
|
||||
// Transform backend response to frontend format
|
||||
return {
|
||||
...activity,
|
||||
timestamp: activity.startedAt,
|
||||
data: activity.metadata,
|
||||
};
|
||||
},
|
||||
|
||||
// Create a new activity
|
||||
createActivity: async (childId: string, data: CreateActivityData): Promise<Activity> => {
|
||||
const response = await apiClient.post(`/api/v1/activities?childId=${childId}`, data);
|
||||
return response.data.data.activity;
|
||||
// Transform frontend data structure to backend DTO format
|
||||
const payload = {
|
||||
type: data.type,
|
||||
startedAt: data.timestamp, // Backend expects startedAt, not timestamp
|
||||
metadata: data.data, // Backend expects metadata, not data
|
||||
notes: data.notes,
|
||||
};
|
||||
const response = await apiClient.post(`/api/v1/activities?childId=${childId}`, payload);
|
||||
const activity = response.data.data.activity;
|
||||
// Transform backend response to frontend format
|
||||
return {
|
||||
...activity,
|
||||
timestamp: activity.startedAt,
|
||||
data: activity.metadata,
|
||||
};
|
||||
},
|
||||
|
||||
// Update an activity
|
||||
|
||||
232
maternal-web/lib/performance/monitoring.ts
Normal file
232
maternal-web/lib/performance/monitoring.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { onCLS, onINP, onFCP, onLCP, onTTFB, type Metric } from 'web-vitals';
|
||||
|
||||
/**
|
||||
* Performance Monitoring Module
|
||||
*
|
||||
* Tracks Core Web Vitals metrics:
|
||||
* - CLS (Cumulative Layout Shift): Measures visual stability
|
||||
* - INP (Interaction to Next Paint): Measures interactivity (replaces FID in v5)
|
||||
* - FCP (First Contentful Paint): Measures perceived load speed
|
||||
* - LCP (Largest Contentful Paint): Measures loading performance
|
||||
* - TTFB (Time to First Byte): Measures server response time
|
||||
*
|
||||
* Sends metrics to analytics (Google Analytics if available)
|
||||
*/
|
||||
|
||||
interface PerformanceMetric {
|
||||
name: string;
|
||||
value: number;
|
||||
id: string;
|
||||
delta: number;
|
||||
rating: 'good' | 'needs-improvement' | 'poor';
|
||||
}
|
||||
|
||||
/**
|
||||
* Send metric to analytics service
|
||||
*/
|
||||
const sendToAnalytics = (metric: Metric) => {
|
||||
const body: PerformanceMetric = {
|
||||
name: metric.name,
|
||||
value: Math.round(metric.value),
|
||||
id: metric.id,
|
||||
delta: metric.delta,
|
||||
rating: metric.rating,
|
||||
};
|
||||
|
||||
// Send to Google Analytics if available
|
||||
if (typeof window !== 'undefined' && (window as any).gtag) {
|
||||
(window as any).gtag('event', metric.name, {
|
||||
event_category: 'Web Vitals',
|
||||
event_label: metric.id,
|
||||
value: Math.round(metric.value),
|
||||
metric_id: metric.id,
|
||||
metric_value: metric.value,
|
||||
metric_delta: metric.delta,
|
||||
metric_rating: metric.rating,
|
||||
non_interaction: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Log to console in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[Performance]', body);
|
||||
}
|
||||
|
||||
// Send to custom analytics endpoint if needed
|
||||
if (process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT) {
|
||||
const analyticsEndpoint = process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT;
|
||||
|
||||
// Use navigator.sendBeacon for reliable analytics even during page unload
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon(
|
||||
analyticsEndpoint,
|
||||
JSON.stringify(body)
|
||||
);
|
||||
} else {
|
||||
// Fallback to fetch
|
||||
fetch(analyticsEndpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
keepalive: true,
|
||||
}).catch((error) => {
|
||||
console.error('Failed to send analytics:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize performance monitoring
|
||||
* Call this function once when the app loads
|
||||
*/
|
||||
export const initPerformanceMonitoring = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Track Cumulative Layout Shift (CLS)
|
||||
// Good: < 0.1, Needs Improvement: < 0.25, Poor: >= 0.25
|
||||
onCLS(sendToAnalytics);
|
||||
|
||||
// Track Interaction to Next Paint (INP) - replaces FID in web-vitals v5
|
||||
// Good: < 200ms, Needs Improvement: < 500ms, Poor: >= 500ms
|
||||
onINP(sendToAnalytics);
|
||||
|
||||
// Track First Contentful Paint (FCP)
|
||||
// Good: < 1.8s, Needs Improvement: < 3s, Poor: >= 3s
|
||||
onFCP(sendToAnalytics);
|
||||
|
||||
// Track Largest Contentful Paint (LCP)
|
||||
// Good: < 2.5s, Needs Improvement: < 4s, Poor: >= 4s
|
||||
onLCP(sendToAnalytics);
|
||||
|
||||
// Track Time to First Byte (TTFB)
|
||||
// Good: < 800ms, Needs Improvement: < 1800ms, Poor: >= 1800ms
|
||||
onTTFB(sendToAnalytics);
|
||||
|
||||
console.log('[Performance] Monitoring initialized');
|
||||
} catch (error) {
|
||||
console.error('[Performance] Failed to initialize monitoring:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Report custom performance metrics
|
||||
*/
|
||||
export const reportCustomMetric = (name: string, value: number, metadata?: Record<string, any>) => {
|
||||
if (typeof window !== 'undefined' && (window as any).gtag) {
|
||||
(window as any).gtag('event', name, {
|
||||
event_category: 'Custom Metrics',
|
||||
value: Math.round(value),
|
||||
...metadata,
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[Performance Custom Metric]', { name, value, metadata });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Measure and report component render time
|
||||
*/
|
||||
export const measureComponentRender = (componentName: string) => {
|
||||
if (typeof window === 'undefined' || !window.performance) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const startMark = `${componentName}-render-start`;
|
||||
const endMark = `${componentName}-render-end`;
|
||||
const measureName = `${componentName}-render`;
|
||||
|
||||
performance.mark(startMark);
|
||||
|
||||
return () => {
|
||||
performance.mark(endMark);
|
||||
performance.measure(measureName, startMark, endMark);
|
||||
|
||||
const measure = performance.getEntriesByName(measureName)[0];
|
||||
if (measure) {
|
||||
reportCustomMetric(`component_render_${componentName}`, measure.duration, {
|
||||
component: componentName,
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up marks and measures
|
||||
performance.clearMarks(startMark);
|
||||
performance.clearMarks(endMark);
|
||||
performance.clearMeasures(measureName);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Track page load time
|
||||
*/
|
||||
export const trackPageLoad = (pageName: string) => {
|
||||
if (typeof window === 'undefined' || !window.performance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for load event
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
||||
if (navigation) {
|
||||
reportCustomMetric(`page_load_${pageName}`, navigation.loadEventEnd - navigation.fetchStart, {
|
||||
page: pageName,
|
||||
dom_content_loaded: navigation.domContentLoadedEventEnd - navigation.fetchStart,
|
||||
dom_interactive: navigation.domInteractive - navigation.fetchStart,
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Monitor long tasks (tasks that block the main thread for > 50ms)
|
||||
*/
|
||||
export const monitorLongTasks = () => {
|
||||
if (typeof window === 'undefined' || !(window as any).PerformanceObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
reportCustomMetric('long_task', entry.duration, {
|
||||
start_time: entry.startTime,
|
||||
duration: entry.duration,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe({ entryTypes: ['longtask'] });
|
||||
} catch (error) {
|
||||
console.error('[Performance] Failed to monitor long tasks:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Track resource loading times
|
||||
*/
|
||||
export const trackResourceTiming = () => {
|
||||
if (typeof window === 'undefined' || !window.performance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
|
||||
|
||||
const slowResources = resources.filter((resource) => resource.duration > 1000);
|
||||
|
||||
slowResources.forEach((resource) => {
|
||||
reportCustomMetric('slow_resource', resource.duration, {
|
||||
url: resource.name,
|
||||
type: resource.initiatorType,
|
||||
size: resource.transferSize,
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user