Skip to content

Commit c7bc2f4

Browse files
Merge pull request #217 from sweetesty/feat/performance-monitoring
feat: implement performance monitoring dashboard and web vitals tracking
2 parents fb8d317 + b29e614 commit c7bc2f4

10 files changed

Lines changed: 2184 additions & 1277 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ NEXT_PUBLIC_STARKNET_NETWORK=goerli-alpha
44

55
# Optional: For production deployments
66
# NEXT_PUBLIC_STARKNET_NETWORK=mainnet-alpha
7+
8+
# Performance Analytics
9+
NEXT_PUBLIC_ENABLE_PERF_ANALYTICS=true

lint-staged.config.js

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,4 @@
1-
const config = {
2-
'*.{js,jsx,ts,tsx}': (filenames) => {
3-
const quoted = filenames.map((f) => `"${f.replace(/"/g, '\\"')}"`).join(' ');
4-
return [
5-
// run eslint --fix on changed files (quoting paths for Windows)
6-
`eslint --max-warnings=0 --fix ${quoted}`,
7-
// then format with prettier
8-
`prettier --write ${quoted}`,
9-
];
10-
},
11-
'*.{json,md,css,scss,html}': (filenames) => {
12-
const quoted = filenames.map((f) => `"${f.replace(/"/g, '\\"')}"`).join(' ');
13-
return [`prettier --write ${quoted}`];
14-
},
15-
'*.{js,jsx,ts,tsx}': (filenames) => [
16-
// run eslint --fix on changed files
17-
`eslint --max-warnings=0 --no-warn-ignored --fix ${filenames.join(' ')}`,
18-
// then format with prettier
19-
`prettier --write ${filenames.join(' ')}`,
20-
],
21-
'*.{json,md,css,scss,html}': (filenames) => [`prettier --write ${filenames.join(' ')}`],
1+
export default {
2+
'*.{js,jsx,ts,tsx}': ['eslint --max-warnings=0 --no-warn-ignored --fix', 'prettier --write'],
3+
'*.{json,md,css,scss,html}': ['prettier --write'],
224
};
23-
24-
export default config;

package-lock.json

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

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
"validate:ui": "node scripts/validate-ui.cjs",
2020
"validate:web3": "node scripts/validate-web3.cjs",
2121
"check-i18n": "node scripts/check-i18n.cjs",
22-
"validate": "npm run validate:ui && npm run validate:web3",
2322
"check-locales": "node scripts/check-locales.mjs",
24-
"prebuild": "npm run check-locales && npm run check-i18n"
23+
"prebuild": "npm run check-locales && npm run check-i18n",
24+
"validate": "npm run validate:ui && npm run validate:web3"
2525
},
2626
"dependencies": {
2727
"@dnd-kit/core": "^6.3.1",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { NextResponse } from 'next/server';
2+
3+
export async function POST(request: Request) {
4+
try {
5+
const metric = await request.json();
6+
7+
// Log the received metric
8+
console.log('[Performance Analytics] Received metric:', {
9+
name: metric.name,
10+
value: metric.value,
11+
rating: metric.rating,
12+
url: metric.url,
13+
timestamp: new Date(metric.timestamp).toISOString(),
14+
});
15+
16+
// Implement alerting logic
17+
if (metric.rating === 'poor') {
18+
console.warn(
19+
`[PERFORMANCE ALERT] Critical degradation detected for ${metric.name} on ${metric.url}. Value: ${metric.value}`,
20+
);
21+
// In a real app, this could trigger a Slack notification, PagerDuty, etc.
22+
} else if (metric.rating === 'needs-improvement') {
23+
console.info(
24+
`[PERFORMANCE WARNING] ${metric.name} needs improvement on ${metric.url}. Value: ${metric.value}`,
25+
);
26+
}
27+
28+
// In a real app, you would store this in a database like PostgreSQL, ClickHouse, or InfluxDB.
29+
// For this demonstration, we'll just acknowledge receipt.
30+
31+
return NextResponse.json({
32+
success: true,
33+
message: 'Metric received and processed',
34+
alertTriggered: metric.rating === 'poor',
35+
});
36+
} catch (error) {
37+
console.error('[Performance Analytics] Error processing metric:', error);
38+
return NextResponse.json({ success: false, message: 'Internal Server Error' }, { status: 500 });
39+
}
40+
}

src/components/performance/PerformanceDashboard.tsx

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@
33
import React, { useMemo } from 'react';
44
import Link from 'next/link';
55
import { Toaster } from 'react-hot-toast';
6-
import { Activity, AlertTriangle, ArrowLeft, Eraser, Trash2 } from 'lucide-react';
6+
import {
7+
Activity,
8+
AlertTriangle,
9+
ArrowLeft,
10+
BarChart3,
11+
Eraser,
12+
Globe,
13+
ShieldCheck,
14+
Trash2,
15+
} from 'lucide-react';
716
import {
817
CartesianGrid,
918
Line,
@@ -43,6 +52,10 @@ export const PerformanceDashboard: React.FC = () => {
4352
const { metrics, alerts, suggestions, trend, clearAlerts, refreshTrendFromStorage } =
4453
usePerformanceMonitoring();
4554

55+
const isAnalyticsEnabled =
56+
process.env.NEXT_PUBLIC_ENABLE_PERF_ANALYTICS === 'true' ||
57+
process.env.NODE_ENV === 'production';
58+
4659
const seriesByVital = useMemo(() => {
4760
const map: Record<string, ReturnType<typeof buildSeries>> = {};
4861
for (const n of VITAL_NAMES) {
@@ -87,7 +100,58 @@ export const PerformanceDashboard: React.FC = () => {
87100
</button>
88101
</header>
89102

90-
<CoreWebVitals metrics={metrics} />
103+
<div className="grid gap-6 md:grid-cols-3">
104+
<div className="md:col-span-2">
105+
<CoreWebVitals metrics={metrics} />
106+
</div>
107+
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900/50 p-4">
108+
<h2 className="text-sm font-semibold flex items-center gap-2 mb-4">
109+
<BarChart3 className="w-4 h-4 text-indigo-500" aria-hidden />
110+
Analytics Status
111+
</h2>
112+
<div className="space-y-4">
113+
<div className="flex items-center justify-between">
114+
<span className="text-xs text-gray-500 dark:text-gray-400">Status</span>
115+
<span
116+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium ${
117+
isAnalyticsEnabled
118+
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
119+
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
120+
}`}
121+
>
122+
{isAnalyticsEnabled ? <ShieldCheck className="w-3 h-3" /> : null}
123+
{isAnalyticsEnabled ? 'Active' : 'Disabled (Dev)'}
124+
</span>
125+
</div>
126+
<div className="flex items-center justify-between">
127+
<span className="text-xs text-gray-500 dark:text-gray-400">Endpoint</span>
128+
<span className="text-xs font-mono text-gray-600 dark:text-gray-300">
129+
/api/performance/vitals
130+
</span>
131+
</div>
132+
<div className="pt-2 border-t border-gray-100 dark:border-gray-800">
133+
<p className="text-[11px] text-gray-500 dark:text-gray-400 mb-2">
134+
<Globe className="inline w-3 h-3 mr-1" />
135+
Simulated Global Average (7d)
136+
</p>
137+
<div className="space-y-1.5">
138+
{[
139+
{ name: 'LCP', val: '1.2s', status: 'good' },
140+
{ name: 'CLS', val: '0.04', status: 'good' },
141+
{ name: 'INP', val: '180ms', status: 'good' },
142+
].map((m) => (
143+
<div key={m.name} className="flex justify-between text-[11px]">
144+
<span className="text-gray-600 dark:text-gray-400">{m.name}</span>
145+
<span className="font-semibold text-emerald-600 dark:text-emerald-400">
146+
{m.val}
147+
</span>
148+
</div>
149+
))}
150+
</div>
151+
</div>
152+
</div>
153+
</div>
154+
</div>
91155

92156
<section aria-labelledby="perf-alerts-heading">
93157
<div className="flex items-center justify-between mb-3">

src/hooks/usePerformanceMonitoring.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
buildOptimizationSuggestions,
1616
evaluatePerformanceDegradation,
1717
loadTrendHistory,
18+
reportVitalToAnalytics,
1819
type PerformanceAlert,
1920
type PerformanceMetric,
2021
type PerformanceTrendPoint,
@@ -63,6 +64,9 @@ function usePerformanceMonitoringState(
6364
setMetrics((prev) => ({ ...prev, [metric.name]: metric }));
6465
onMetricRef.current?.(metric);
6566

67+
// Report to analytics service
68+
reportVitalToAnalytics(metric);
69+
6670
if (enableTrendRecording) {
6771
const point: PerformanceTrendPoint = {
6872
t: Date.now(),

src/tsconfig.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
"jsx": "preserve",
1515
"incremental": true,
1616
"baseUrl": ".",
17-
"typeRoots": ["../node_modules/@types"],
1817
"plugins": [
1918
{
2019
"name": "next"

src/types/shims.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module 'linkify-it';

src/utils/performanceUtils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,3 +324,47 @@ export function runWhenIdle(callback: () => void, timeoutMs = 2000): void {
324324
window.setTimeout(callback, 1);
325325
}
326326
}
327+
328+
/**
329+
* Report a performance metric to the analytics service.
330+
*/
331+
export async function reportVitalToAnalytics(metric: PerformanceMetric): Promise<void> {
332+
if (typeof window === 'undefined') return;
333+
334+
// Only report in production or if explicitly enabled
335+
const shouldReport =
336+
process.env.NODE_ENV === 'production' ||
337+
process.env.NEXT_PUBLIC_ENABLE_PERF_ANALYTICS === 'true';
338+
339+
if (!shouldReport) {
340+
console.debug(`[Performance Analytics] Skipping report for ${metric.name} in development`);
341+
return;
342+
}
343+
344+
runWhenIdle(async () => {
345+
try {
346+
const body = JSON.stringify({
347+
...metric,
348+
timestamp: Date.now(),
349+
url: window.location.href,
350+
userAgent: navigator.userAgent,
351+
});
352+
353+
const response = await fetch('/api/performance/vitals', {
354+
method: 'POST',
355+
headers: { 'Content-Type': 'application/json' },
356+
body,
357+
// Use keepalive to ensure the request is sent even if the page is closed
358+
keepalive: true,
359+
});
360+
361+
if (!response.ok) {
362+
console.warn(
363+
`[Performance Analytics] Failed to send ${metric.name}: ${response.statusText}`,
364+
);
365+
}
366+
} catch (err) {
367+
console.error(`[Performance Analytics] Error sending ${metric.name}:`, err);
368+
}
369+
});
370+
}

0 commit comments

Comments
 (0)