Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import OfflineBanner from './components/OfflineBanner';
import Onboarding from './components/Onboarding';
import { AnimatedHero } from './components/ui/animated-hero';
import { ToastContainer, useToast } from './components/ui/toast';
import { analytics } from './lib/analytics';

const TipHistory = lazy(() => import('./components/TipHistory'));
const PlatformStats = lazy(() => import('./components/PlatformStats'));
Expand All @@ -27,18 +28,26 @@ function App() {
if (userSession.isUserSignedIn()) {
setUserData(userSession.loadUserData());
}
analytics.trackSession();
}, []);

useEffect(() => {
analytics.trackPageView(location.pathname);
analytics.trackTabNavigation(location.pathname);
}, [location.pathname]);

const handleAuth = async () => {
if (userData) {
disconnect();
setUserData(null);
analytics.trackWalletDisconnect();
return;
}

setAuthLoading(true);
try {
await authenticate();
analytics.trackWalletConnect();
} catch (error) {
console.error('Authentication failed:', error.message || error);
addToast(error.message || 'Failed to connect wallet. Please try again.', 'error');
Expand Down
103 changes: 103 additions & 0 deletions frontend/src/components/AdminDashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { network, appDetails, userSession } from '../utils/stacks';
import { CONTRACT_ADDRESS, CONTRACT_NAME } from '../config/contracts';
import { formatSTX } from '../lib/utils';
import { analytics } from '../lib/analytics';

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

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

useEffect(() => {
fetchAdminData();
setAnalyticsData(analytics.getSummary());
}, [fetchAdminData]);

const handlePauseToggle = async () => {
Expand Down Expand Up @@ -157,6 +160,104 @@ export default function AdminDashboard({ addToast }) {
);
}

const AnalyticsPanel = () => {
if (!analyticsData) return null;
const a = analyticsData;

return (
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 space-y-5">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold text-gray-800">Usage Analytics</h3>
<button
onClick={() => setAnalyticsData(analytics.getSummary())}
className="text-xs text-gray-500 hover:text-gray-700 font-medium"
>
Refresh
</button>
</div>

<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="bg-gray-50 rounded-xl p-3 text-center">
<p className="text-2xl font-black text-gray-900">{a.totalPageViews}</p>
<p className="text-xs text-gray-500 mt-0.5">Page Views</p>
</div>
<div className="bg-gray-50 rounded-xl p-3 text-center">
<p className="text-2xl font-black text-gray-900">{a.walletConnections}</p>
<p className="text-xs text-gray-500 mt-0.5">Wallet Connects</p>
</div>
<div className="bg-gray-50 rounded-xl p-3 text-center">
<p className="text-2xl font-black text-gray-900">{a.sessions}</p>
<p className="text-xs text-gray-500 mt-0.5">Sessions</p>
</div>
<div className="bg-gray-50 rounded-xl p-3 text-center">
<p className="text-2xl font-black text-gray-900">{a.totalErrors}</p>
<p className="text-xs text-gray-500 mt-0.5">Errors</p>
</div>
</div>

<div>
<h4 className="text-sm font-semibold text-gray-700 mb-2">Tip Funnel</h4>
<div className="space-y-1.5">
{[
['Started', a.tipsStarted],
['Submitted', a.tipsSubmitted],
['Confirmed', a.tipsConfirmed],
['Cancelled', a.tipsCancelled],
['Failed', a.tipsFailed],
].map(([label, count]) => (
<div key={label} className="flex items-center justify-between text-sm">
<span className="text-gray-600">{label}</span>
<span className="font-semibold text-gray-900">{count}</span>
</div>
))}
<div className="border-t border-gray-100 pt-1.5 flex items-center justify-between text-sm">
<span className="text-gray-600">Completion Rate</span>
<span className="font-bold text-green-600">{a.tipCompletionRate}%</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Drop-off Rate</span>
<span className="font-bold text-orange-500">{a.tipDropOffRate}%</span>
</div>
</div>
</div>

{a.sortedTabs.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-gray-700 mb-2">Tab Navigation</h4>
<div className="space-y-1">
{a.sortedTabs.map(([tab, count]) => (
<div key={tab} className="flex items-center justify-between text-sm">
<span className="text-gray-600 font-mono text-xs">{tab}</span>
<span className="font-semibold text-gray-900">{count}</span>
</div>
))}
</div>
</div>
)}

{a.sortedErrors.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-gray-700 mb-2">Top Errors</h4>
<div className="space-y-1">
{a.sortedErrors.map(([error, count]) => (
<div key={error} className="flex items-center justify-between text-sm">
<span className="text-gray-600 truncate max-w-[70%]" title={error}>{error}</span>
<span className="font-semibold text-red-600">{count}</span>
</div>
))}
</div>
</div>
)}

{a.firstSeen && (
<p className="text-xs text-gray-400 text-right">
Tracking since {new Date(a.firstSeen).toLocaleDateString()}
</p>
)}
</div>
);
};

return (
<div className="max-w-2xl mx-auto space-y-6">
<h2 className="text-2xl font-bold text-gray-800">Admin Dashboard</h2>
Expand Down Expand Up @@ -223,6 +324,8 @@ export default function AdminDashboard({ addToast }) {
</button>
</div>
</div>

<AnalyticsPanel />
</div>
);
}
4 changes: 4 additions & 0 deletions frontend/src/components/BatchTip.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { network, appDetails, userSession } from '../utils/stacks';
import { CONTRACT_ADDRESS, CONTRACT_NAME } from '../config/contracts';
import { toMicroSTX, formatSTX } from '../lib/utils';
import { useTipContext } from '../context/TipContext';
import { analytics } from '../lib/analytics';

const MAX_RECIPIENTS = 50;
const MIN_TIP_STX = 0.001;
Expand Down Expand Up @@ -94,6 +95,7 @@ export default function BatchTip({ addToast }) {
const handleSubmit = async () => {
if (!validate()) return;
setSending(true);
analytics.trackBatchTipStarted();

try {
const totalMicro = toMicroSTX(totalAmount + totalFee);
Expand Down Expand Up @@ -123,6 +125,7 @@ export default function BatchTip({ addToast }) {
],
onFinish: () => {
notifyTipSent();
analytics.trackBatchTipSubmitted();
addToast(`Batch of ${validEntries.length} tips submitted`, 'success');
setEntries([emptyEntry(), emptyEntry()]);
},
Expand All @@ -131,6 +134,7 @@ export default function BatchTip({ addToast }) {
},
});
} catch (err) {
analytics.trackError('BatchTip', err.message || 'Unknown error');
addToast(err.message || 'Failed to send batch tips', 'error');
} finally {
setSending(false);
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/ErrorBoundary.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Component } from 'react';
import { analytics } from '../lib/analytics';

export default class ErrorBoundary extends Component {
constructor(props) {
Expand All @@ -12,6 +13,7 @@ export default class ErrorBoundary extends Component {

componentDidCatch(error, info) {
console.error('Uncaught error:', error, info.componentStack);
analytics.trackError('ErrorBoundary', error.message || 'Unknown error');
}

handleReset = () => {
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/components/SendTip.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { toMicroSTX, formatSTX } from '../lib/utils';
import { useTipContext } from '../context/TipContext';
import { useBalance } from '../hooks/useBalance';
import { useStxPrice } from '../hooks/useStxPrice';
import { analytics } from '../lib/analytics';
import ConfirmDialog from './ui/confirm-dialog';
import TxStatus from './ui/tx-status';

Expand Down Expand Up @@ -148,10 +149,12 @@ export default function SendTip({ addToast }) {
}

setShowConfirm(true);
analytics.trackTipStarted();
};

const handleSendTip = async () => {
setShowConfirm(false);
analytics.trackTipSubmitted();

setLoading(true);

Expand Down Expand Up @@ -191,18 +194,22 @@ export default function SendTip({ addToast }) {
notifyTipSent();
refetchBalance();
startCooldown();
analytics.trackTipConfirmed();
addToast('Tip sent successfully! Transaction: ' + data.txId, 'success');
},
onCancel: () => {
console.info('Transaction cancelled by user');
setLoading(false);
analytics.trackTipCancelled();
addToast('Transaction cancelled. Your funds were not transferred.', 'info');
}
};

await openContractCall(options);
} catch (error) {
console.error('Failed to send tip:', error.message || error);
analytics.trackTipFailed();
analytics.trackError('SendTip', error.message || 'Unknown error');
addToast('Failed to send tip. Please try again.', 'error');
setLoading(false);
}
Expand Down
Loading
Loading