Overview
Event tracking is the cornerstone of product analytics. Every meaningful user interaction, business outcome, and system event should be captured and analyzed. This guide covers how to implement event tracking systematically across your product.
The goal: track everything that matters, ignore what doesn't, and maintain consistency as your product evolves.
Event Tracking Strategy
What to Track
User lifecycle events:
- Signup, login, logout
- Onboarding completion
- Profile updates
- Account deletion
Feature engagement:
- Feature activation
- Feature usage frequency
- Feature-specific actions
- Feature abandonment
Conversion funnels:
- Add to cart
- Checkout initiated
- Payment completed
- Trial started
- Subscription created
Errors and issues:
Content interaction:
- Page views
- Content viewed (articles, videos)
- Search queries
- Downloads
Implementation Patterns
Page View Tracking
Automatic (default):
posthog.init('YOUR_API_KEY', {
capture_pageview: true // Tracks pageviews automatically
});
Manual (SPAs):
// Initialize without auto-pageview
posthog.init('YOUR_API_KEY', {
capture_pageview: false
});
// Track manually on route change
function trackPageView(pageName) {
posthog.capture('$pageview', {
$current_url: window.location.href,
$pathname: window.location.pathname,
page_title: document.title,
page_name: pageName
});
}
// React Router example
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';
function App() {
const location = useLocation();
useEffect(() => {
trackPageView(location.pathname);
}, [location]);
return <YourApp />;
}
Click Tracking
Autocapture (recommended for exploration):
posthog.init('YOUR_API_KEY', {
autocapture: true // Automatically tracks all clicks
});
Manual (recommended for precision):
// Track specific button clicks
document.querySelector('#signup-button').addEventListener('click', () => {
posthog.capture('signup_button_clicked', {
button_location: 'hero',
button_text: 'Start Free Trial',
page_url: window.location.href
});
});
// React example
function SignupButton() {
const handleClick = () => {
posthog.capture('signup_button_clicked', {
button_location: 'hero',
button_text: 'Start Free Trial'
});
};
return <button Free Trial</button>;
}
Form Tracking
Form submission:
document.querySelector('#contact-form').addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(e.target);
posthog.capture('contact_form_submitted', {
form_name: 'contact',
form_location: 'footer',
has_message: formData.get('message').length > 0,
message_length: formData.get('message').length
// Don't send actual form content for privacy
});
// Submit form
submitForm(formData);
});
Form field interactions:
// Track which fields users interact with
document.querySelectorAll('input, textarea, select').forEach(field => {
field.addEventListener('focus', () => {
posthog.capture('form_field_focused', {
field_name: field.name,
field_type: field.type,
form_name: field.closest('form')?.name
});
});
});
Video Tracking
Video playback events:
const video = document.querySelector('video');
video.addEventListener('play', () => {
posthog.capture('video_played', {
video_id: video.dataset.videoId,
video_title: video.dataset.title,
video_duration_seconds: video.duration,
playback_position_seconds: video.currentTime
});
});
video.addEventListener('pause', () => {
posthog.capture('video_paused', {
video_id: video.dataset.videoId,
playback_position_seconds: video.currentTime,
watch_duration_seconds: video.currentTime
});
});
video.addEventListener('ended', () => {
posthog.capture('video_completed', {
video_id: video.dataset.videoId,
video_duration_seconds: video.duration,
completion_percent: 100
});
});
// Track viewing milestones
const milestones = [0.25, 0.50, 0.75];
video.addEventListener('timeupdate', () => {
const progress = video.currentTime / video.duration;
milestones.forEach(milestone => {
if (progress >= milestone && !video.dataset[`milestone_${milestone}`]) {
video.dataset[`milestone_${milestone}`] = 'true';
posthog.capture('video_progress', {
video_id: video.dataset.videoId,
milestone_percent: milestone * 100
});
}
});
});
Scroll Depth Tracking
let maxScrollDepth = 0;
window.addEventListener('scroll', () => {
const scrollPercent = Math.round(
(window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
);
if (scrollPercent > maxScrollDepth) {
maxScrollDepth = scrollPercent;
}
});
// Track on page exit
window.addEventListener('beforeunload', () => {
posthog.capture('page_scroll_depth', {
max_scroll_percent: maxScrollDepth,
page_url: window.location.href,
page_title: document.title
});
});
// Or track milestones
const scrollMilestones = [25, 50, 75, 100];
let trackedMilestones = [];
window.addEventListener('scroll', () => {
const scrollPercent = Math.round(
(window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
);
scrollMilestones.forEach(milestone => {
if (scrollPercent >= milestone && !trackedMilestones.includes(milestone)) {
trackedMilestones.push(milestone);
posthog.capture('scroll_milestone_reached', {
milestone_percent: milestone,
page_url: window.location.href
});
}
});
});
E-commerce Tracking
Complete purchase funnel:
// 1. Product viewed
posthog.capture('product_viewed', {
product_id: 'SKU_123',
product_name: 'Wireless Headphones',
product_category: 'Electronics > Audio',
price: 199.99,
currency: 'USD',
in_stock: true
});
// 2. Added to cart
posthog.capture('product_added_to_cart', {
product_id: 'SKU_123',
product_name: 'Wireless Headphones',
price: 199.99,
quantity: 1,
cart_total: 199.99,
cart_item_count: 1,
source: 'product_page'
});
// 3. Cart viewed
posthog.capture('cart_viewed', {
cart_total: 199.99,
cart_item_count: 1,
product_ids: ['SKU_123']
});
// 4. Checkout started
posthog.capture('checkout_started', {
cart_total: 199.99,
cart_item_count: 1,
checkout_step: 1
});
// 5. Shipping info entered
posthog.capture('checkout_step_completed', {
checkout_step: 1,
step_name: 'shipping_info',
shipping_method: 'standard'
});
// 6. Payment info entered
posthog.capture('checkout_step_completed', {
checkout_step: 2,
step_name: 'payment_info',
payment_method: 'credit_card'
});
// 7. Purchase completed
posthog.capture('purchase_completed', {
order_id: 'ORDER_12345',
revenue: 199.99,
tax: 16.00,
shipping: 10.00,
total: 225.99,
currency: 'USD',
item_count: 1,
payment_method: 'credit_card',
is_first_purchase: true
});
Error Tracking
Client-side errors:
window.addEventListener('error', (event) => {
posthog.capture('javascript_error', {
error_message: event.message,
error_filename: event.filename,
error_line: event.lineno,
error_column: event.colno,
error_stack: event.error?.stack,
page_url: window.location.href
});
});
// Promise rejections
window.addEventListener('unhandledrejection', (event) => {
posthog.capture('unhandled_promise_rejection', {
error_message: event.reason?.message || String(event.reason),
error_stack: event.reason?.stack,
page_url: window.location.href
});
});
API errors:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
posthog.capture('api_error', {
endpoint: url,
status_code: response.status,
status_text: response.statusText,
method: 'GET'
});
}
return await response.json();
} catch (error) {
posthog.capture('api_error', {
endpoint: url,
error_message: error.message,
error_type: 'network_error'
});
throw error;
}
}
Search Tracking
document.querySelector('#search-form').addEventListener('submit', (e) => {
e.preventDefault();
const query = e.target.querySelector('input[name="q"]').value;
posthog.capture('search_performed', {
search_query: query,
search_location: 'header',
query_length: query.length
});
// Execute search
performSearch(query);
});
// Track search results
function displaySearchResults(query, results) {
posthog.capture('search_results_viewed', {
search_query: query,
result_count: results.length,
has_results: results.length > 0
});
// Display results
}
// Track search result clicks
function trackSearchResultClick(query, result, position) {
posthog.capture('search_result_clicked', {
search_query: query,
result_id: result.id,
result_title: result.title,
result_position: position
});
}
Feature Flag Usage Tracking
// PostHog automatically tracks feature flag evaluations
// But you can also track when features are actually used
posthog.onFeatureFlags(() => {
if (posthog.isFeatureEnabled('new-dashboard')) {
// Show new dashboard
showNewDashboard();
// Track that user saw the feature
posthog.capture('feature_flag_active', {
flag_key: 'new-dashboard',
flag_value: true
});
}
});
// Track feature-specific interactions
function useNewDashboardFeature(featureName) {
posthog.capture('new_dashboard_feature_used', {
feature_name: featureName,
feature_flag: 'new-dashboard'
});
}
Server-Side Event Tracking
Node.js API example:
const { PostHog } = require('posthog-node');
const posthog = new PostHog('YOUR_API_KEY');
// Track subscription creation
app.post('/api/subscriptions', async (req, res) => {
const subscription = await createSubscription(req.user.id, req.body);
posthog.capture({
distinctId: req.user.id,
event: 'subscription_created',
properties: {
plan_name: subscription.plan,
plan_price: subscription.price,
billing_cycle: subscription.billing_cycle,
payment_method: subscription.paymentMethod,
trial_days: subscription.trialDays
}
});
res.json(subscription);
});
// Track background job completion
async function processDataExport(userId, exportId) {
const startTime = Date.now();
try {
await generateExport(userId, exportId);
posthog.capture({
distinctId: userId,
event: 'data_export_completed',
properties: {
export_id: exportId,
duration_ms: Date.now() - startTime,
status: 'success'
}
});
} catch (error) {
posthog.capture({
distinctId: userId,
event: 'data_export_failed',
properties: {
export_id: exportId,
duration_ms: Date.now() - startTime,
error_message: error.message,
status: 'failed'
}
});
throw error;
}
}
// Always shutdown on process exit
process.on('SIGTERM', async () => {
await posthog.shutdown();
process.exit(0);
});
Testing Event Tracking
Manual testing:
// Enable debug mode
posthog.debug = true;
// Perform action that should trigger event
// Check browser console for:
// [PostHog] Event captured: your_event_name
// Check PostHog Activity within 30 seconds
Automated testing (Playwright):
const { test, expect } = require('@playwright/test');
test('tracks signup button click', async ({ page }) => {
// Intercept PostHog requests
const events = [];
page.on('request', request => {
if (request.url().includes('posthog.com')) {
const postData = request.postDataJSON();
if (postData?.batch) {
events.push(...postData.batch);
}
}
});
// Navigate and click
await page.goto('/');
await page.click('#signup-button');
// Wait for event to be sent
await page.waitForTimeout(1000);
// Verify event was captured
const signupEvent = events.find(e => e.event === 'signup_button_clicked');
expect(signupEvent).toBeDefined();
expect(signupEvent.properties.button_location).toBe('hero');
});
Validation and Testing Procedures
Step 1: Verify Event Tracking Setup
Check PostHog initialization:
// verification-script.js
function verifyPostHogSetup() {
const checks = {
loaded: typeof posthog !== 'undefined',
initialized: false,
distinctId: null,
config: null,
autocapture: null
};
if (checks.loaded) {
checks.initialized = !!posthog._init_session;
checks.distinctId = posthog.get_distinct_id();
checks.config = {
api_host: posthog.config.api_host,
autocapture: posthog.config.autocapture,
capture_pageview: posthog.config.capture_pageview,
loaded: posthog.config.loaded
};
}
console.log('=== PostHog Setup Verification ===');
console.table(checks);
if (!checks.loaded) {
console.error('PostHog not loaded');
return false;
}
if (!checks.initialized) {
console.warn('PostHog not fully initialized');
return false;
}
console.log('PostHog setup verified');
return true;
}
// Run on page load
verifyPostHogSetup();
Step 2: Test Event Capture
Create test harness:
// event-test-harness.js
class EventTestHarness {
constructor() {
this.capturedEvents = [];
this.setupInterceptor();
}
setupInterceptor() {
const originalCapture = posthog.capture;
const self = this;
posthog.capture = function(eventName, properties) {
// Record event
self.capturedEvents.push({
event: eventName,
properties: properties,
timestamp: Date.now(),
url: window.location.href
});
console.log(`Event captured: ${eventName}`, properties);
// Call original
return originalCapture.call(this, eventName, properties);
};
console.log('Event test harness initialized');
}
getEvents() {
return this.capturedEvents;
}
getEventByName(eventName) {
return this.capturedEvents.filter(e => e.event === eventName);
}
assertEventCaptured(eventName, minCount = 1) {
const events = this.getEventByName(eventName);
const captured = events.length >= minCount;
console.assert(
captured,
`Expected at least ${minCount} "${eventName}" event(s), found ${events.length}`
);
return captured;
}
assertPropertyExists(eventName, propertyName) {
const events = this.getEventByName(eventName);
if (events.length === 0) {
console.error(`No events found with name: ${eventName}`);
return false;
}
const hasProperty = events.some(e =>
e.properties && propertyName in e.properties
);
console.assert(
hasProperty,
`Expected property "${propertyName}" in event "${eventName}"`
);
return hasProperty;
}
reset() {
this.capturedEvents = [];
console.log('Event test harness reset');
}
printReport() {
console.log('\n=== Event Tracking Report ===');
console.log(`Total events captured: ${this.capturedEvents.length}\n`);
const eventCounts = this.capturedEvents.reduce((acc, e) => {
acc[e.event] = (acc[e.event] || 0) + 1;
return acc;
}, {});
console.log('Events by type:');
console.table(eventCounts);
console.log('\nRecent events:');
console.table(this.capturedEvents.slice(-10));
}
}
// Usage
const testHarness = new EventTestHarness();
// Perform actions...
// Then verify
testHarness.assertEventCaptured('button_clicked');
testHarness.assertPropertyExists('button_clicked', 'button_name');
testHarness.printReport();
Step 3: Validate Event Properties
Property validation test:
// validate-event-properties.js
function validateEventProperties(eventName, properties, schema) {
const errors = [];
// Check required properties
if (schema.required) {
schema.required.forEach(prop => {
if (!(prop in properties)) {
errors.push(`Missing required property: ${prop}`);
}
});
}
// Check property types
if (schema.types) {
Object.entries(schema.types).forEach(([prop, expectedType]) => {
if (prop in properties) {
const actualType = typeof properties[prop];
if (actualType !== expectedType) {
errors.push(
`Property "${prop}": expected ${expectedType}, got ${actualType}`
);
}
}
});
}
// Check for forbidden properties (PII)
const forbiddenProps = ['password', 'ssn', 'credit_card', 'api_key'];
Object.keys(properties).forEach(prop => {
if (forbiddenProps.some(forbidden => prop.includes(forbidden))) {
errors.push(`Forbidden property detected: ${prop} (potential PII)`);
}
});
if (errors.length > 0) {
console.error(`Validation errors for "${eventName}":`, errors);
return false;
}
return true;
}
// Define schema
const buttonClickSchema = {
required: ['button_name', 'button_location'],
types: {
button_name: 'string',
button_location: 'string',
button_text: 'string'
}
};
// Validate before tracking
const eventProps = {
button_name: 'signup_cta',
button_location: 'hero',
button_text: 'Start Free Trial'
};
if (validateEventProperties('button_clicked', eventProps, buttonClickSchema)) {
posthog.capture('button_clicked', eventProps);
}
Step 4: Test Event Flow
User journey tracking test:
// test-event-flow.js
class EventFlowTester {
constructor() {
this.journey = [];
this.startTime = Date.now();
}
trackStep(stepName, eventName, properties = {}) {
const step = {
stepName,
eventName,
properties,
timestamp: Date.now(),
timeSinceStart: Date.now() - this.startTime
};
this.journey.push(step);
// Track in PostHog
posthog.capture(eventName, {
...properties,
journey_step: stepName,
step_number: this.journey.length,
time_since_start_ms: step.timeSinceStart
});
console.log(`Step ${this.journey.length}: ${stepName}`);
}
validateFlow(expectedSteps) {
const actualSteps = this.journey.map(j => j.stepName);
const isValid = expectedSteps.every((step, index) =>
actualSteps[index] === step
);
if (!isValid) {
console.error('Flow validation failed');
console.log('Expected:', expectedSteps);
console.log('Actual:', actualSteps);
return false;
}
console.log('Event flow validated successfully');
return true;
}
getReport() {
return {
totalSteps: this.journey.length,
totalDuration: Date.now() - this.startTime,
steps: this.journey,
averageStepTime: (Date.now() - this.startTime) / this.journey.length
};
}
printReport() {
const report = this.getReport();
console.log('\n=== Event Flow Report ===');
console.log(`Total steps: ${report.totalSteps}`);
console.log(`Total duration: ${report.totalDuration}ms`);
console.log(`Average step time: ${report.averageStepTime.toFixed(2)}ms\n`);
console.table(report.steps);
}
}
// Usage - Test signup flow
const flowTest = new EventFlowTester();
flowTest.trackStep('Landing', 'page_viewed', { page: 'home' });
flowTest.trackStep('CTA Click', 'button_clicked', { button: 'signup' });
flowTest.trackStep('Form Start', 'signup_form_started', {});
flowTest.trackStep('Form Submit', 'signup_form_submitted', {});
flowTest.trackStep('Confirmation', 'signup_completed', {});
// Validate expected flow
flowTest.validateFlow([
'Landing',
'CTA Click',
'Form Start',
'Form Submit',
'Confirmation'
]);
flowTest.printReport();
Step 5: Browser Testing
Cross-browser event tracking test:
// browser-compatibility-test.js
async function testBrowserCompatibility() {
const results = {
browser: navigator.userAgent,
tests: []
};
// Test 1: PostHog loaded
results.tests.push({
test: 'PostHog SDK loaded',
passed: typeof posthog !== 'undefined',
details: typeof posthog !== 'undefined' ? 'Loaded' : 'Not found'
});
// Test 2: Event capture works
try {
posthog.capture('browser_test_event', { test: true });
results.tests.push({
test: 'Event capture',
passed: true,
details: 'Successfully captured test event'
});
} catch (e) {
results.tests.push({
test: 'Event capture',
passed: false,
details: e.message
});
}
// Test 3: Local storage available
results.tests.push({
test: 'LocalStorage available',
passed: typeof localStorage !== 'undefined',
details: typeof localStorage !== 'undefined' ? 'Available' : 'Not available'
});
// Test 4: Cookies enabled
results.tests.push({
test: 'Cookies enabled',
passed: navigator.cookieEnabled,
details: navigator.cookieEnabled ? 'Enabled' : 'Disabled'
});
// Test 5: Network request works
const networkTest = await new Promise(resolve => {
const img = new Image();
const timeout = setTimeout(() => resolve(false), 5000);
img.onload = () => {
clearTimeout(timeout);
resolve(true);
};
img.onerror = () => {
clearTimeout(timeout);
resolve(false);
};
img.src = 'https://app.posthog.com/decide/?v=3';
});
results.tests.push({
test: 'Network connectivity',
passed: networkTest,
details: networkTest ? 'PostHog reachable' : 'Connection failed'
});
console.log('\n=== Browser Compatibility Test ===');
console.log(`Browser: ${results.browser}\n`);
console.table(results.tests);
const allPassed = results.tests.every(t => t.passed);
console.log(allPassed ? 'All tests passed' : 'Some tests failed');
return results;
}
// Run test
testBrowserCompatibility();
Step 6: Production Monitoring
Real-time event monitoring:
// production-event-monitor.js
class ProductionEventMonitor {
constructor() {
this.metrics = {
totalEvents: 0,
eventTypes: {},
errors: 0,
startTime: Date.now()
};
this.setupMonitoring();
}
setupMonitoring() {
const originalCapture = posthog.capture;
const self = this;
posthog.capture = function(eventName, properties) {
// Update metrics
self.metrics.totalEvents++;
self.metrics.eventTypes[eventName] =
(self.metrics.eventTypes[eventName] || 0) + 1;
// Validate event
if (!eventName || typeof eventName !== 'string') {
self.metrics.errors++;
console.error('Invalid event name:', eventName);
}
// Check for common issues
if (properties) {
// Check for undefined values
Object.entries(properties).forEach(([key, value]) => {
if (value === undefined) {
console.warn(`Undefined value for property "${key}" in event "${eventName}"`);
}
});
}
// Call original
return originalCapture.call(this, eventName, properties);
};
// Log metrics periodically
setInterval(() => this.logMetrics(), 60000); // Every minute
}
getMetrics() {
const uptime = Date.now() - this.metrics.startTime;
return {
totalEvents: this.metrics.totalEvents,
eventTypes: this.metrics.eventTypes,
errors: this.metrics.errors,
eventsPerMinute: (this.metrics.totalEvents / (uptime / 60000)).toFixed(2),
uptimeSeconds: Math.floor(uptime / 1000)
};
}
logMetrics() {
const metrics = this.getMetrics();
console.log('=== Event Monitoring Metrics ===');
console.log(`Total events: ${metrics.totalEvents}`);
console.log(`Events/minute: ${metrics.eventsPerMinute}`);
console.log(`Errors: ${metrics.errors}`);
console.log(`Uptime: ${metrics.uptimeSeconds}s\n`);
console.log('Event types:');
console.table(metrics.eventTypes);
}
reset() {
this.metrics = {
totalEvents: 0,
eventTypes: {},
errors: 0,
startTime: Date.now()
};
}
}
// Initialize in production
if (typeof posthog !== 'undefined') {
const monitor = new ProductionEventMonitor();
// Expose globally for debugging
window.posthogMonitor = monitor;
}
Troubleshooting
Common Issues and Solutions
| Problem | Symptoms | Root Cause | Solution |
|---|---|---|---|
| Events not appearing in PostHog | Tracked events don't show in dashboard | Ad blocker enabled, network issues, or wrong API key | Check browser console for errors; disable ad blockers; verify API key; check network tab |
| Duplicate events | Same event appears multiple times | Multiple event listeners or re-renders | Use event delegation; debounce handlers; check React useEffect dependencies |
| Events missing properties | Properties not appearing in PostHog | Properties undefined at capture time | Validate properties before tracking; check for async timing issues |
| Autocapture not working | No automatic click events | Autocapture disabled or elements not captured | Enable autocapture: true in config; check element attributes |
| Page views not tracked | $pageview events missing | Manual pageview tracking in SPA without implementation | Call posthog.capture('$pageview') on route changes |
| Events sent after user leaves | Events lost on navigation | No time for request to complete | Use posthog.flush() before navigation; use sendBeacon |
| Form submissions not tracked | Form events missing | Event.preventDefault() before tracking | Track event first, then prevent default and submit |
| Video events inconsistent | Video tracking sporadic | Event listeners not properly attached | Ensure video element loaded before adding listeners |
| Scroll tracking inaccurate | Wrong scroll depth values | Calculation errors or timing issues | Use debounced scroll handler; verify calculation logic |
| Error tracking missing context | Error events lack useful info | Insufficient error details captured | Include stack trace, user context, and page state |
| Search tracking incomplete | Search queries not captured | Form submission handler missing | Track on both input change and form submit |
| Feature flag events delayed | Flag evaluation events late | Waiting for flags to load | Use posthog.onFeatureFlags() callback |
| Server-side events not attributed | Server events show as different users | Distinct ID not passed from client | Send distinct_id in API requests from client |
| Events blocked by CSP | PostHog requests blocked | Content Security Policy restrictions | Add PostHog domain to CSP whitelist |
| Mobile events inconsistent | Tracking unreliable on mobile | Network connectivity issues | Implement retry logic; use local queue |
| Debug mode not showing events | Console doesn't show event logs | Debug mode not enabled | Set posthog.debug() or debug: true in config |
| Properties have wrong types | Numbers as strings, etc. | Type coercion issues | Explicitly cast types before tracking |
| Events rate limited | Some events not processed | Exceeding PostHog rate limits | Reduce event volume; batch events; upgrade plan |
| Timestamp issues | Events show wrong time | Timezone or format problems | Use ISO 8601 UTC timestamps |
| Anonymous users not merging | Identified users show as new | Missing alias call | Use posthog.alias() when identifying users |
| React events stale data | Old state values in events | Closure capturing stale state | Use refs or functional updates for current values |
Debug Commands
Enable debug mode:
// Method 1: In initialization
posthog.init('YOUR_API_KEY', {
debug: true
});
// Method 2: After initialization
posthog.debug();
// Method 3: In browser console
posthog.debug(true);
Check event queue:
// View pending events
console.log('Pending events:', posthog._events_queue);
// Force flush
posthog.flush();
Inspect configuration:
// View current config
console.log('PostHog config:', posthog.config);
// Check specific settings
console.log('Autocapture:', posthog.config.autocapture);
console.log('API host:', posthog.config.api_host);
console.log('Capture pageview:', posthog.config.capture_pageview);
Test network connectivity:
// Test if PostHog is reachable
fetch('https://app.posthog.com/decide/?v=3')
.then(res => console.log('PostHog reachable:', res.ok))
.catch(err => console.error('PostHog unreachable:', err));
Validation Checklist
Before deploying event tracking to production:
- PostHog SDK loaded and initialized correctly
- API key validated and working
- All critical events defined and documented
- Event naming convention followed (snake_case)
- Required properties validated before tracking
- No PII tracked in event properties
- Page view tracking configured (auto or manual)
- Form submission tracking implemented
- Error tracking setup and tested
- Feature-specific events implemented
- Conversion funnel events tracked
- Event flows tested end-to-end
- Cross-browser compatibility verified
- Mobile responsiveness tested
- Network failure handling implemented
- Debug mode tested in development
- Events appearing in PostHog dashboard
- User identification working correctly
- Anonymous to identified user flow tested
- Production monitoring implemented
Best Practices
Do:
- Track events at the moment they happen, not after
- Include relevant context in properties
- Use consistent naming conventions
- Track both success and failure states
- Test event tracking before production deploy
- Document what each event means
Don't:
- Track PII (emails, names) in event properties without hashing
- Create events for every possible interaction (be selective)
- Use events for debugging (use proper logging instead)
- Track the same action multiple times
- Forget to track error states
Common Patterns
Track with context:
function trackWithContext(event, properties = {}) {
posthog.capture(event, {
...properties,
page_url: window.location.href,
page_title: document.title,
timestamp: new Date().toISOString()
});
}
Debounced tracking:
const debounce = (fn, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
};
// Track search input changes (debounced)
const trackSearchInput = debounce((query) => {
posthog.capture('search_input_changed', {
query_length: query.length,
has_query: query.length > 0
});
}, 500);
searchInput.addEventListener('input', (e) => {
trackSearchInput(e.target.value);
});
Need help? Check the troubleshooting guide or PostHog event tracking docs.