Examples
React SPA (Vite/CRA)
For the Vite and Create React App folks
React SPA Example
Not using Next.js? No problem. Here's the setup for Vite, Create React App, or any other React SPA.
Project Structure
main.tsx
App.tsx
.env
Setup
Install Dependencies
pnpm add @thisbefine/analytics react-router-domnpm install @thisbefine/analytics react-router-domyarn add @thisbefine/analytics react-router-domSet Up App Entry
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { Analytics, BugReportFAB } from '@thisbefine/analytics/react';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
<Analytics
apiKey={import.meta.env.VITE_THISBEFINE_API_KEY}
debug={import.meta.env.DEV}
config={{
errors: {
enabled: true,
captureNetworkErrors: !import.meta.env.DEV,
},
}}
/>
<BugReportFAB />
</BrowserRouter>
</React.StrictMode>
);Using Create React App? Replace import.meta.env.VITE_THISBEFINE_API_KEY with process.env.REACT_APP_THISBEFINE_API_KEY.
Configure Router
import { Routes, Route } from 'react-router-dom';
import { Home } from './pages/Home';
import { Login } from './pages/Login';
import { Dashboard } from './pages/Dashboard';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
);
}
export default App;Done. Now let's add some actual tracking.
Authentication Example
Login Page
import { useIdentify, useTrack } from '@thisbefine/analytics/react';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
export const Login = () => {
const identify = useIdentify();
const trackLogin = useTrack('login_completed');
const trackLoginFailed = useTrack('login_failed');
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const data = await response.json();
trackLoginFailed({ reason: data.error || 'invalid_credentials' });
setError(data.message || 'Invalid credentials');
return;
}
const { user, token } = await response.json();
// Store token
localStorage.setItem('token', token);
// Identify the user
identify(user.id, {
email: user.email,
name: user.name,
plan: user.plan,
createdAt: user.createdAt,
});
// Track the login
trackLogin({ method: 'email' });
// Off to the dashboard
navigate('/dashboard');
} catch (err) {
trackLoginFailed({ reason: 'network_error' });
setError('Network error. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="login-page">
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
{error && <p className="error">{error}</p>}
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
</div>
);
};Logout Hook
import { useReset, useTrack } from '@thisbefine/analytics/react';
import { useNavigate } from 'react-router-dom';
export const useLogout = () => {
const reset = useReset();
const trackLogout = useTrack('logout');
const navigate = useNavigate();
const logout = async () => {
// Track before reset
trackLogout();
// Clear local storage
localStorage.removeItem('token');
// Reset analytics
reset();
// Navigate to login
navigate('/login');
};
return { logout };
};Feature Tracking
Dashboard with Event Tracking
import { useTrack, useLog } from '@thisbefine/analytics/react';
import { useState } from 'react';
export const Dashboard = () => {
const trackExport = useTrack('data_exported');
const trackFilter = useTrack('filter_applied');
const log = useLog();
const [filter, setFilter] = useState('all');
const [exporting, setExporting] = useState(false);
const handleFilterChange = (newFilter: string) => {
setFilter(newFilter);
trackFilter({ filter: newFilter, previousFilter: filter });
};
const handleExport = async (format: 'csv' | 'json' | 'pdf') => {
setExporting(true);
log('Export started', 'info', { format });
try {
const response = await fetch(`/api/export?format=${format}`);
const blob = await response.blob();
// Trigger download
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `data.${format}`;
a.click();
trackExport({
format,
filter,
success: true,
});
log('Export completed', 'info', { format });
} catch (error) {
trackExport({
format,
filter,
success: false,
error: (error as Error).message,
});
log('Export failed', 'error', {
format,
error: (error as Error).message,
});
} finally {
setExporting(false);
}
};
return (
<div className="dashboard">
<h1>Dashboard</h1>
<div className="filters">
<select value={filter} onChange={(e) => handleFilterChange(e.target.value)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="archived">Archived</option>
</select>
</div>
<div className="export-buttons">
<button onClick={() => handleExport('csv')} disabled={exporting}>
Export CSV
</button>
<button onClick={() => handleExport('json')} disabled={exporting}>
Export JSON
</button>
<button onClick={() => handleExport('pdf')} disabled={exporting}>
Export PDF
</button>
</div>
</div>
);
};Error Boundary
import { Component, ReactNode } from 'react';
import { getAnalytics } from '@thisbefine/analytics';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Send to Thisbefine
getAnalytics()?.captureException(error, {
componentStack: errorInfo.componentStack,
boundary: true,
});
}
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div style={{ padding: '2rem', textAlign: 'center' }}>
<h2>Oops! Something went wrong.</h2>
<p style={{ color: '#666' }}>{this.state.error?.message}</p>
<button
onClick={() => {
this.setState({ hasError: false, error: null });
window.location.reload();
}}
>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
// Wrap your app
// <ErrorBoundary>
// <App />
// </ErrorBoundary>Environment Variables
VITE_THISBEFINE_API_KEY=tif_your_api_keyAccess with import.meta.env.VITE_THISBEFINE_API_KEY
REACT_APP_THISBEFINE_API_KEY=tif_your_api_keyAccess with process.env.REACT_APP_THISBEFINE_API_KEY
Never commit your .env file to source control. Add it to .gitignore.
TypeScript Configuration
Using TypeScript with Vite? Add environment type declarations:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_THISBEFINE_API_KEY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}Now TypeScript knows about your environment variables. No more red squiggly lines.