398 lines
8.7 KiB
Markdown
398 lines
8.7 KiB
Markdown
# 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>
|
|
);
|
|
}
|
|
```
|