Examples
Next.js App Router
The full setup for Next.js (yes, App Router)
Next.js App Router Example
A complete, copy-paste-ready setup for Next.js App Router. No guessing required.
Project Structure
Here's what you'll end up with:
layout.tsx
page.tsx
login/page.tsx
layout.tsx
dashboard/page.tsx
settings/page.tsx
.env.local
Setup
Add to Root Layout
import { Analytics, BugReportFAB } from '@thisbefine/analytics/next';
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Analytics
apiKey={process.env.NEXT_PUBLIC_THISBEFINE_API_KEY!}
debug={process.env.NODE_ENV === 'development'}
config={{
errors: {
enabled: true,
captureNetworkErrors: process.env.NODE_ENV === 'production',
maxBreadcrumbs: 50,
},
}}
/>
<BugReportFAB />
</body>
</html>
);
}Done. Every page now tracks pageviews. Every error gets captured. Users can report bugs.
Zero config option: Set NEXT_PUBLIC_TBF_API_KEY in your .env.local and you can use <Analytics /> with no props at all.
Authentication Flow
The important bits: identify on login, reset on logout.
Login Page
'use client';
import { useIdentify, useTrack } from '@thisbefine/analytics/next';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function LoginPage() {
const identify = useIdentify();
const trackLogin = useTrack('login_completed');
const trackLoginFailed = useTrack('login_failed');
const router = useRouter();
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({
email: formData.get('email'),
password: formData.get('password'),
}),
});
if (!response.ok) {
trackLoginFailed({ reason: 'invalid_credentials' });
setError('Invalid credentials');
return;
}
const { user } = await response.json();
// Step 1: Tell us who they are
identify(user.id, {
email: user.email,
name: user.name,
plan: user.plan,
});
// Step 2: Track the login
trackLogin({ method: 'email' });
// Step 3: Send them on their way
router.push('/dashboard');
} catch (err) {
trackLoginFailed({ reason: 'network_error' });
setError('Something went wrong');
}
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" required />
{error && <p className="text-red-500">{error}</p>}
<button type="submit">Login</button>
</form>
);
}Logout Handler
'use client';
import { useReset, useTrack } from '@thisbefine/analytics/next';
import { useRouter } from 'next/navigation';
export const LogoutButton = () => {
const reset = useReset();
const trackLogout = useTrack('logout');
const router = useRouter();
const handleLogout = async () => {
// Track BEFORE reset (otherwise we lose the userId)
trackLogout();
// Call your logout API
await fetch('/api/auth/logout', { method: 'POST' });
// Clear analytics identity
reset();
router.push('/login');
};
return <button onClick={handleLogout}>Logout</button>;
};Feature Tracking
Track Button Clicks
'use client';
import { useTrack } from '@thisbefine/analytics/next';
interface CTAButtonProps {
variant: 'primary' | 'secondary';
location: string;
children: React.ReactNode;
}
export const CTAButton = ({ variant, location, children }: CTAButtonProps) => {
const trackClick = useTrack('cta_clicked');
return (
<button
onClick={() => trackClick({ variant, location })}
className={variant === 'primary' ? 'bg-blue-500' : 'bg-gray-500'}
>
{children}
</button>
);
};Track Feature Usage
'use client';
import { useTrack } from '@thisbefine/analytics/next';
export default function DashboardPage() {
const trackExport = useTrack('data_exported');
const handleExport = async (format: 'csv' | 'json') => {
trackExport({ format });
// Your export logic...
};
return (
<div>
<h1>Dashboard</h1>
<button onClick={() => handleExport('csv')}>Export CSV</button>
<button onClick={() => handleExport('json')}>Export JSON</button>
</div>
);
}Workspace/Team Context
For B2B apps with workspaces or teams:
'use client';
import { useGroup } from '@thisbefine/analytics/next';
import { useEffect } from 'react';
interface Workspace {
id: string;
name: string;
plan: string;
memberCount: number;
}
export const useWorkspaceAnalytics = (workspace: Workspace | null) => {
const group = useGroup();
useEffect(() => {
if (workspace) {
group(workspace.id, {
name: workspace.name,
plan: workspace.plan,
employeeCount: workspace.memberCount,
});
}
}, [workspace, group]);
};Error Boundary
Catch React errors and send them to Thisbefine:
'use client';
import { Component, ReactNode } from 'react';
import { getAnalytics } from '@thisbefine/analytics';
interface Props {
children: 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,
});
}
render() {
if (this.state.hasError) {
return (
<div className="p-8 text-center">
<h2>Something went wrong</h2>
<p className="text-muted-foreground">{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}Environment Variables
NEXT_PUBLIC_THISBEFINE_API_KEY=tif_your_api_key_hereNever commit .env.local. Make sure it's in your .gitignore.
That's it. You now have analytics, error tracking, and bug reports. Time to ship.