analytics/docs/react-integration.md

399 lines
8.7 KiB
Markdown
Raw Permalink Normal View History

# React Integration Guide
Integrate analytics into React applications with hooks and context.
## Installation
```bash
npm install @analytics/client
```
## Setup
### 1. Create Analytics Provider
```tsx
// analytics-provider.tsx
import { createContext, useContext, useMemo, type ReactNode } from 'react';
import { AnalyticsClient, type AnalyticsConfig } from '@analytics/client';
interface AnalyticsContextValue {
client: AnalyticsClient;
trackEvent: (type: string, action: string, metadata?: Record<string, unknown>) => void;
identify: (userId: string, traits?: Record<string, unknown>) => void;
}
const AnalyticsContext = createContext<AnalyticsContextValue | null>(null);
interface AnalyticsProviderProps {
children: ReactNode;
config: AnalyticsConfig;
}
export function AnalyticsProvider({ children, config }: AnalyticsProviderProps) {
const value = useMemo(() => {
const client = new AnalyticsClient(config);
return {
client,
trackEvent: (type, action, metadata) => {
client.trackEngagement({ type, action, metadata });
},
identify: (userId, traits) => {
client.identify(userId, traits);
},
};
}, [config]);
return (
<AnalyticsContext.Provider value={value}>
{children}
</AnalyticsContext.Provider>
);
}
export function useAnalytics() {
const context = useContext(AnalyticsContext);
if (!context) {
throw new Error('useAnalytics must be used within AnalyticsProvider');
}
return context;
}
```
### 2. Wrap Your App
```tsx
// App.tsx
import { AnalyticsProvider } from './analytics-provider';
function App() {
return (
<AnalyticsProvider
config={{
apiBaseUrl: import.meta.env.VITE_ANALYTICS_URL,
appName: 'my-react-app',
enabled: import.meta.env.PROD,
}}
>
<Router />
</AnalyticsProvider>
);
}
```
## Hooks
### usePageTracking
Track page views on route changes.
```tsx
// hooks/use-page-tracking.ts
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useAnalytics } from '../analytics-provider';
export function usePageTracking() {
const location = useLocation();
const { trackEvent } = useAnalytics();
useEffect(() => {
trackEvent('navigation', 'page_view', {
path: location.pathname,
search: location.search,
});
}, [location.pathname, location.search, trackEvent]);
}
// Usage: Call once in your root component
function AppContent() {
usePageTracking();
return <Outlet />;
}
```
### useClickTracking
Track element clicks.
```tsx
// hooks/use-click-tracking.ts
import { useCallback } from 'react';
import { useAnalytics } from '../analytics-provider';
export function useClickTracking() {
const { trackEvent } = useAnalytics();
return useCallback((
elementName: string,
metadata?: Record<string, unknown>
) => {
trackEvent('click', elementName, metadata);
}, [trackEvent]);
}
// Usage
function CTAButton() {
const trackClick = useClickTracking();
return (
<button onClick={() => trackClick('signup_cta', { location: 'hero' })}>
Sign Up
</button>
);
}
```
### useFormTracking
Track form interactions.
```tsx
// hooks/use-form-tracking.ts
import { useCallback } from 'react';
import { useAnalytics } from '../analytics-provider';
export function useFormTracking(formName: string) {
const { trackEvent } = useAnalytics();
const trackStart = useCallback(() => {
trackEvent('form', 'start', { formName });
}, [trackEvent, formName]);
const trackField = useCallback((fieldName: string) => {
trackEvent('form', 'field_focus', { formName, fieldName });
}, [trackEvent, formName]);
const trackSubmit = useCallback((success: boolean, error?: string) => {
trackEvent('form', success ? 'submit_success' : 'submit_error', {
formName,
success,
error,
});
}, [trackEvent, formName]);
const trackAbandonment = useCallback((lastField?: string) => {
trackEvent('form', 'abandonment', { formName, lastField });
}, [trackEvent, formName]);
return { trackStart, trackField, trackSubmit, trackAbandonment };
}
```
### useScrollTracking
Track scroll depth.
```tsx
// hooks/use-scroll-tracking.ts
import { useEffect, useRef } from 'react';
import { useAnalytics } from '../analytics-provider';
export function useScrollTracking() {
const { trackEvent } = useAnalytics();
const trackedDepths = useRef(new Set<number>());
useEffect(() => {
const thresholds = [25, 50, 75, 90, 100];
const handleScroll = () => {
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollPercent = Math.round((window.scrollY / scrollHeight) * 100);
for (const threshold of thresholds) {
if (scrollPercent >= threshold && !trackedDepths.current.has(threshold)) {
trackedDepths.current.add(threshold);
trackEvent('scroll', 'depth', { percent: threshold });
}
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [trackEvent]);
}
```
## Component Patterns
### Tracked Link
```tsx
import { Link, type LinkProps } from 'react-router-dom';
import { useClickTracking } from '../hooks/use-click-tracking';
interface TrackedLinkProps extends LinkProps {
trackingName: string;
trackingMetadata?: Record<string, unknown>;
}
export function TrackedLink({
trackingName,
trackingMetadata,
onClick,
...props
}: TrackedLinkProps) {
const trackClick = useClickTracking();
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
trackClick(trackingName, trackingMetadata);
onClick?.(e);
};
return <Link {...props} onClick={handleClick} />;
}
```
### Tracked Button
```tsx
interface TrackedButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
trackingName: string;
trackingMetadata?: Record<string, unknown>;
}
export function TrackedButton({
trackingName,
trackingMetadata,
onClick,
...props
}: TrackedButtonProps) {
const trackClick = useClickTracking();
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
trackClick(trackingName, trackingMetadata);
onClick?.(e);
};
return <button {...props} onClick={handleClick} />;
}
```
### Impression Tracking
Track when elements become visible.
```tsx
import { useEffect, useRef } from 'react';
import { useAnalytics } from '../analytics-provider';
interface UseImpressionTrackingOptions {
elementName: string;
metadata?: Record<string, unknown>;
threshold?: number;
trackOnce?: boolean;
}
export function useImpressionTracking({
elementName,
metadata,
threshold = 0.5,
trackOnce = true,
}: UseImpressionTrackingOptions) {
const { trackEvent } = useAnalytics();
const ref = useRef<HTMLElement>(null);
const hasTracked = useRef(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
if (trackOnce && hasTracked.current) return;
hasTracked.current = true;
trackEvent('impression', elementName, metadata);
}
},
{ threshold }
);
observer.observe(element);
return () => observer.disconnect();
}, [elementName, metadata, threshold, trackOnce, trackEvent]);
return ref;
}
// Usage
function ProductCard({ product }) {
const ref = useImpressionTracking({
elementName: 'product_card',
metadata: { productId: product.id },
});
return <div ref={ref}>...</div>;
}
```
## Best Practices
### 1. Track at the Right Level
```tsx
// ❌ Don't track everything
onClick={() => {
trackClick('button');
trackClick('cta');
trackClick('hero_cta');
}}
// ✅ Track meaningful, specific events
onClick={() => trackClick('hero_signup_cta')}
```
### 2. Use Consistent Naming
```tsx
// ❌ Inconsistent naming
trackEvent('click', 'SignUp');
trackEvent('user_action', 'sign-up');
trackEvent('button', 'signup_button');
// ✅ Consistent naming convention
trackEvent('click', 'signup_cta');
trackEvent('click', 'login_cta');
trackEvent('click', 'pricing_link');
```
### 3. Include Context
```tsx
// ❌ Missing context
trackEvent('click', 'buy_button');
// ✅ Rich context
trackEvent('click', 'buy_button', {
productId: product.id,
price: product.price,
location: 'product_page',
variant: selectedVariant,
});
```
### 4. Handle Loading States
```tsx
function AnalyticsWrapper({ children }) {
const [ready, setReady] = useState(false);
useEffect(() => {
// Initialize analytics
setReady(true);
}, []);
if (!ready) {
// Render children without tracking during init
return <>{children}</>;
}
return (
<AnalyticsProvider config={...}>
{children}
</AnalyticsProvider>
);
}
```