Skip to content

Commit ca959b1

Browse files
authored
Merge pull request #186 from Mosas2000/feature/analytics-tracking
feat: analytics and metrics tracking for platform growth
2 parents c4c1b6b + 5d7367b commit ca959b1

File tree

7 files changed

+387
-0
lines changed

7 files changed

+387
-0
lines changed

frontend/src/App.jsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import OfflineBanner from './components/OfflineBanner';
77
import Onboarding from './components/Onboarding';
88
import { AnimatedHero } from './components/ui/animated-hero';
99
import { ToastContainer, useToast } from './components/ui/toast';
10+
import { analytics } from './lib/analytics';
1011

1112
const TipHistory = lazy(() => import('./components/TipHistory'));
1213
const PlatformStats = lazy(() => import('./components/PlatformStats'));
@@ -27,18 +28,26 @@ function App() {
2728
if (userSession.isUserSignedIn()) {
2829
setUserData(userSession.loadUserData());
2930
}
31+
analytics.trackSession();
3032
}, []);
3133

34+
useEffect(() => {
35+
analytics.trackPageView(location.pathname);
36+
analytics.trackTabNavigation(location.pathname);
37+
}, [location.pathname]);
38+
3239
const handleAuth = async () => {
3340
if (userData) {
3441
disconnect();
3542
setUserData(null);
43+
analytics.trackWalletDisconnect();
3644
return;
3745
}
3846

3947
setAuthLoading(true);
4048
try {
4149
await authenticate();
50+
analytics.trackWalletConnect();
4251
} catch (error) {
4352
console.error('Authentication failed:', error.message || error);
4453
addToast(error.message || 'Failed to connect wallet. Please try again.', 'error');

frontend/src/components/AdminDashboard.jsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { network, appDetails, userSession } from '../utils/stacks';
1111
import { CONTRACT_ADDRESS, CONTRACT_NAME } from '../config/contracts';
1212
import { formatSTX } from '../lib/utils';
13+
import { analytics } from '../lib/analytics';
1314

1415
export default function AdminDashboard({ addToast }) {
1516
const [stats, setStats] = useState(null);
@@ -19,6 +20,7 @@ export default function AdminDashboard({ addToast }) {
1920
const [loading, setLoading] = useState(true);
2021
const [actionLoading, setActionLoading] = useState(false);
2122
const [isOwner, setIsOwner] = useState(false);
23+
const [analyticsData, setAnalyticsData] = useState(null);
2224

2325
const userAddress = userSession.isUserSignedIn()
2426
? userSession.loadUserData().profile.stxAddress.mainnet
@@ -72,6 +74,7 @@ export default function AdminDashboard({ addToast }) {
7274

7375
useEffect(() => {
7476
fetchAdminData();
77+
setAnalyticsData(analytics.getSummary());
7578
}, [fetchAdminData]);
7679

7780
const handlePauseToggle = async () => {
@@ -157,6 +160,104 @@ export default function AdminDashboard({ addToast }) {
157160
);
158161
}
159162

163+
const AnalyticsPanel = () => {
164+
if (!analyticsData) return null;
165+
const a = analyticsData;
166+
167+
return (
168+
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 space-y-5">
169+
<div className="flex items-center justify-between">
170+
<h3 className="text-lg font-bold text-gray-800">Usage Analytics</h3>
171+
<button
172+
onClick={() => setAnalyticsData(analytics.getSummary())}
173+
className="text-xs text-gray-500 hover:text-gray-700 font-medium"
174+
>
175+
Refresh
176+
</button>
177+
</div>
178+
179+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
180+
<div className="bg-gray-50 rounded-xl p-3 text-center">
181+
<p className="text-2xl font-black text-gray-900">{a.totalPageViews}</p>
182+
<p className="text-xs text-gray-500 mt-0.5">Page Views</p>
183+
</div>
184+
<div className="bg-gray-50 rounded-xl p-3 text-center">
185+
<p className="text-2xl font-black text-gray-900">{a.walletConnections}</p>
186+
<p className="text-xs text-gray-500 mt-0.5">Wallet Connects</p>
187+
</div>
188+
<div className="bg-gray-50 rounded-xl p-3 text-center">
189+
<p className="text-2xl font-black text-gray-900">{a.sessions}</p>
190+
<p className="text-xs text-gray-500 mt-0.5">Sessions</p>
191+
</div>
192+
<div className="bg-gray-50 rounded-xl p-3 text-center">
193+
<p className="text-2xl font-black text-gray-900">{a.totalErrors}</p>
194+
<p className="text-xs text-gray-500 mt-0.5">Errors</p>
195+
</div>
196+
</div>
197+
198+
<div>
199+
<h4 className="text-sm font-semibold text-gray-700 mb-2">Tip Funnel</h4>
200+
<div className="space-y-1.5">
201+
{[
202+
['Started', a.tipsStarted],
203+
['Submitted', a.tipsSubmitted],
204+
['Confirmed', a.tipsConfirmed],
205+
['Cancelled', a.tipsCancelled],
206+
['Failed', a.tipsFailed],
207+
].map(([label, count]) => (
208+
<div key={label} className="flex items-center justify-between text-sm">
209+
<span className="text-gray-600">{label}</span>
210+
<span className="font-semibold text-gray-900">{count}</span>
211+
</div>
212+
))}
213+
<div className="border-t border-gray-100 pt-1.5 flex items-center justify-between text-sm">
214+
<span className="text-gray-600">Completion Rate</span>
215+
<span className="font-bold text-green-600">{a.tipCompletionRate}%</span>
216+
</div>
217+
<div className="flex items-center justify-between text-sm">
218+
<span className="text-gray-600">Drop-off Rate</span>
219+
<span className="font-bold text-orange-500">{a.tipDropOffRate}%</span>
220+
</div>
221+
</div>
222+
</div>
223+
224+
{a.sortedTabs.length > 0 && (
225+
<div>
226+
<h4 className="text-sm font-semibold text-gray-700 mb-2">Tab Navigation</h4>
227+
<div className="space-y-1">
228+
{a.sortedTabs.map(([tab, count]) => (
229+
<div key={tab} className="flex items-center justify-between text-sm">
230+
<span className="text-gray-600 font-mono text-xs">{tab}</span>
231+
<span className="font-semibold text-gray-900">{count}</span>
232+
</div>
233+
))}
234+
</div>
235+
</div>
236+
)}
237+
238+
{a.sortedErrors.length > 0 && (
239+
<div>
240+
<h4 className="text-sm font-semibold text-gray-700 mb-2">Top Errors</h4>
241+
<div className="space-y-1">
242+
{a.sortedErrors.map(([error, count]) => (
243+
<div key={error} className="flex items-center justify-between text-sm">
244+
<span className="text-gray-600 truncate max-w-[70%]" title={error}>{error}</span>
245+
<span className="font-semibold text-red-600">{count}</span>
246+
</div>
247+
))}
248+
</div>
249+
</div>
250+
)}
251+
252+
{a.firstSeen && (
253+
<p className="text-xs text-gray-400 text-right">
254+
Tracking since {new Date(a.firstSeen).toLocaleDateString()}
255+
</p>
256+
)}
257+
</div>
258+
);
259+
};
260+
160261
return (
161262
<div className="max-w-2xl mx-auto space-y-6">
162263
<h2 className="text-2xl font-bold text-gray-800">Admin Dashboard</h2>
@@ -223,6 +324,8 @@ export default function AdminDashboard({ addToast }) {
223324
</button>
224325
</div>
225326
</div>
327+
328+
<AnalyticsPanel />
226329
</div>
227330
);
228331
}

frontend/src/components/BatchTip.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { network, appDetails, userSession } from '../utils/stacks';
1313
import { CONTRACT_ADDRESS, CONTRACT_NAME } from '../config/contracts';
1414
import { toMicroSTX, formatSTX } from '../lib/utils';
1515
import { useTipContext } from '../context/TipContext';
16+
import { analytics } from '../lib/analytics';
1617

1718
const MAX_RECIPIENTS = 50;
1819
const MIN_TIP_STX = 0.001;
@@ -94,6 +95,7 @@ export default function BatchTip({ addToast }) {
9495
const handleSubmit = async () => {
9596
if (!validate()) return;
9697
setSending(true);
98+
analytics.trackBatchTipStarted();
9799

98100
try {
99101
const totalMicro = toMicroSTX(totalAmount + totalFee);
@@ -123,6 +125,7 @@ export default function BatchTip({ addToast }) {
123125
],
124126
onFinish: () => {
125127
notifyTipSent();
128+
analytics.trackBatchTipSubmitted();
126129
addToast(`Batch of ${validEntries.length} tips submitted`, 'success');
127130
setEntries([emptyEntry(), emptyEntry()]);
128131
},
@@ -131,6 +134,7 @@ export default function BatchTip({ addToast }) {
131134
},
132135
});
133136
} catch (err) {
137+
analytics.trackError('BatchTip', err.message || 'Unknown error');
134138
addToast(err.message || 'Failed to send batch tips', 'error');
135139
} finally {
136140
setSending(false);

frontend/src/components/ErrorBoundary.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Component } from 'react';
2+
import { analytics } from '../lib/analytics';
23

34
export default class ErrorBoundary extends Component {
45
constructor(props) {
@@ -12,6 +13,7 @@ export default class ErrorBoundary extends Component {
1213

1314
componentDidCatch(error, info) {
1415
console.error('Uncaught error:', error, info.componentStack);
16+
analytics.trackError('ErrorBoundary', error.message || 'Unknown error');
1517
}
1618

1719
handleReset = () => {

frontend/src/components/SendTip.jsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { toMicroSTX, formatSTX } from '../lib/utils';
1313
import { useTipContext } from '../context/TipContext';
1414
import { useBalance } from '../hooks/useBalance';
1515
import { useStxPrice } from '../hooks/useStxPrice';
16+
import { analytics } from '../lib/analytics';
1617
import ConfirmDialog from './ui/confirm-dialog';
1718
import TxStatus from './ui/tx-status';
1819

@@ -148,10 +149,12 @@ export default function SendTip({ addToast }) {
148149
}
149150

150151
setShowConfirm(true);
152+
analytics.trackTipStarted();
151153
};
152154

153155
const handleSendTip = async () => {
154156
setShowConfirm(false);
157+
analytics.trackTipSubmitted();
155158

156159
setLoading(true);
157160

@@ -191,18 +194,22 @@ export default function SendTip({ addToast }) {
191194
notifyTipSent();
192195
refetchBalance();
193196
startCooldown();
197+
analytics.trackTipConfirmed();
194198
addToast('Tip sent successfully! Transaction: ' + data.txId, 'success');
195199
},
196200
onCancel: () => {
197201
console.info('Transaction cancelled by user');
198202
setLoading(false);
203+
analytics.trackTipCancelled();
199204
addToast('Transaction cancelled. Your funds were not transferred.', 'info');
200205
}
201206
};
202207

203208
await openContractCall(options);
204209
} catch (error) {
205210
console.error('Failed to send tip:', error.message || error);
211+
analytics.trackTipFailed();
212+
analytics.trackError('SendTip', error.message || 'Unknown error');
206213
addToast('Failed to send tip. Please try again.', 'error');
207214
setLoading(false);
208215
}

0 commit comments

Comments
 (0)